mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
Resolved conflicts: - openai_source.py: keep dev version with abort_signal filtering - customizer.ts: keep dev version with viewMode functionality - useSessions.ts: keep dev version with pendingSessionId handling - platformUtils.js: keep dev version with correct tutorial links - AddNewPlatform.vue: keep dev version with correct docs link - FullLayout.vue: keep dev version with viewMode-based logic - VerticalHeader.vue: keep dev version with viewMode-based logic
1224 lines
40 KiB
Vue
1224 lines
40 KiB
Vue
<template>
|
||
<v-dialog
|
||
v-model="showDialog"
|
||
:max-width="$vuetify.display.smAndDown ? undefined : '1200px'"
|
||
scrollable
|
||
>
|
||
<v-card
|
||
class="persona-form-card"
|
||
:class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }"
|
||
>
|
||
<v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6">
|
||
{{
|
||
editingPersona ? tm("dialog.edit.title") : tm("dialog.create.title")
|
||
}}
|
||
</v-card-title>
|
||
|
||
<v-card-text class="persona-form-content">
|
||
<!-- 创建位置提示 -->
|
||
<v-alert
|
||
v-if="!editingPersona"
|
||
type="info"
|
||
variant="tonal"
|
||
density="compact"
|
||
class="mb-4"
|
||
icon="mdi-folder-outline"
|
||
>
|
||
{{ tm("form.createInFolder", { folder: folderDisplayName }) }}
|
||
</v-alert>
|
||
|
||
<v-form ref="personaForm" v-model="formValid">
|
||
<v-row class="persona-form-layout">
|
||
<v-col cols="12" md="6" class="persona-basic-col">
|
||
<v-text-field
|
||
v-model="personaForm.persona_id"
|
||
:label="tm('form.personaId')"
|
||
:rules="personaIdRules"
|
||
:disabled="editingPersona"
|
||
variant="outlined"
|
||
density="comfortable"
|
||
class="mb-4"
|
||
/>
|
||
|
||
<v-textarea
|
||
v-model="personaForm.system_prompt"
|
||
:label="tm('form.systemPrompt')"
|
||
:rules="systemPromptRules"
|
||
variant="outlined"
|
||
rows="16"
|
||
class="mb-4"
|
||
/>
|
||
|
||
<v-textarea
|
||
v-model="personaForm.custom_error_message"
|
||
:label="tm('form.customErrorMessage')"
|
||
:hint="tm('form.customErrorMessageHelp')"
|
||
variant="outlined"
|
||
rows="4"
|
||
persistent-hint
|
||
clearable
|
||
class="mb-4"
|
||
/>
|
||
</v-col>
|
||
|
||
<v-col cols="12" md="6" class="persona-panels-col">
|
||
<v-expansion-panels v-model="expandedPanels" multiple>
|
||
<!-- 工具选择面板 -->
|
||
<v-expansion-panel value="tools">
|
||
<v-expansion-panel-title>
|
||
<v-icon class="mr-2"> mdi-tools </v-icon>
|
||
{{ tm("form.tools") }}
|
||
<v-chip
|
||
v-if="
|
||
Array.isArray(personaForm.tools) &&
|
||
personaForm.tools.length > 0
|
||
"
|
||
size="small"
|
||
color="primary"
|
||
variant="tonal"
|
||
class="ml-2"
|
||
>
|
||
{{ personaForm.tools.length }}
|
||
</v-chip>
|
||
</v-expansion-panel-title>
|
||
|
||
<v-expansion-panel-text>
|
||
<div class="mb-3">
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.toolsHelp") }}
|
||
</p>
|
||
</div>
|
||
|
||
<v-radio-group
|
||
v-model="toolSelectValue"
|
||
class="mt-2"
|
||
hide-details="true"
|
||
>
|
||
<v-radio label="默认使用全部函数工具" value="0" />
|
||
<v-radio label="选择指定函数工具" value="1" />
|
||
</v-radio-group>
|
||
|
||
<div
|
||
v-if="toolSelectValue === '1'"
|
||
class="mt-3 selected-config-area"
|
||
>
|
||
<!-- 工具搜索 -->
|
||
<v-text-field
|
||
v-model="toolSearch"
|
||
:label="tm('form.searchTools')"
|
||
prepend-inner-icon="mdi-magnify"
|
||
variant="outlined"
|
||
density="compact"
|
||
hide-details
|
||
clearable
|
||
class="mb-3"
|
||
/>
|
||
|
||
<!-- MCP 服务器 -->
|
||
<div v-if="mcpServers.length > 0" class="mb-4">
|
||
<h4 class="text-subtitle-2 mb-2">
|
||
{{ tm("form.mcpServersQuickSelect") }}
|
||
</h4>
|
||
<div class="d-flex flex-wrap ga-2">
|
||
<v-chip
|
||
v-for="server in mcpServers"
|
||
:key="server.name"
|
||
:color="
|
||
isServerSelected(server) ? 'primary' : 'default'
|
||
"
|
||
:variant="
|
||
isServerSelected(server) ? 'flat' : 'outlined'
|
||
"
|
||
size="small"
|
||
clickable
|
||
:disabled="
|
||
!server.tools || server.tools.length === 0
|
||
"
|
||
@click="toggleMcpServer(server)"
|
||
>
|
||
<v-icon start size="small"> mdi-server </v-icon>
|
||
{{ server.name }}
|
||
<v-chip-text v-if="server.tools" class="ml-1">
|
||
({{ server.tools.length }})
|
||
</v-chip-text>
|
||
</v-chip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工具选择列表 -->
|
||
<div
|
||
v-if="filteredTools.length > 0"
|
||
class="tools-selection"
|
||
>
|
||
<v-virtual-scroll
|
||
:items="filteredTools"
|
||
height="300"
|
||
item-height="72"
|
||
>
|
||
<template #default="{ item }">
|
||
<v-list-item
|
||
:key="item.name"
|
||
density="comfortable"
|
||
@click="toggleTool(item.name)"
|
||
>
|
||
<template #prepend>
|
||
<v-checkbox-btn
|
||
:model-value="isToolSelected(item.name)"
|
||
@click.stop="toggleTool(item.name)"
|
||
/>
|
||
</template>
|
||
|
||
<v-list-item-title>
|
||
{{ item.name }}
|
||
|
||
<v-chip
|
||
v-if="item.origin"
|
||
size="x-small"
|
||
color="info"
|
||
class="mr-2"
|
||
variant="tonal"
|
||
>
|
||
{{ item.origin }}
|
||
</v-chip>
|
||
<v-chip
|
||
v-if="item.origin_name"
|
||
size="x-small"
|
||
color="info"
|
||
variant="outlined"
|
||
>
|
||
{{ item.origin_name }}
|
||
</v-chip>
|
||
</v-list-item-title>
|
||
|
||
<v-list-item-subtitle v-if="item.description">
|
||
{{ truncateText(item.description, 100) }}
|
||
</v-list-item-subtitle>
|
||
</v-list-item>
|
||
</template>
|
||
</v-virtual-scroll>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="!loadingTools && availableTools.length === 0"
|
||
class="text-center pa-4"
|
||
>
|
||
<v-icon size="48" color="grey-lighten-2" class="mb-2">
|
||
mdi-tools
|
||
</v-icon>
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noToolsAvailable") }}
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="!loadingTools && filteredTools.length === 0"
|
||
class="text-center pa-4"
|
||
>
|
||
<v-icon size="48" color="grey-lighten-2" class="mb-2">
|
||
mdi-magnify
|
||
</v-icon>
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noToolsFound") }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loadingTools" class="text-center pa-4">
|
||
<v-progress-circular indeterminate color="primary" />
|
||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||
{{ tm("form.loadingTools") }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 已选择的工具 -->
|
||
<div class="mt-4">
|
||
<h4 class="text-subtitle-2 mb-2">
|
||
{{ tm("form.selectedTools") }}
|
||
<span
|
||
v-if="personaForm.tools === null"
|
||
class="text-success"
|
||
>
|
||
({{ tm("form.allSelected") }})
|
||
</span>
|
||
<span v-else-if="Array.isArray(personaForm.tools)">
|
||
({{ personaForm.tools.length }})
|
||
</span>
|
||
</h4>
|
||
<div
|
||
v-if="
|
||
Array.isArray(personaForm.tools) &&
|
||
personaForm.tools.length > 0
|
||
"
|
||
class="d-flex flex-wrap ga-1"
|
||
style="max-height: 100px; overflow-y: auto"
|
||
>
|
||
<v-chip
|
||
v-for="toolName in personaForm.tools"
|
||
:key="toolName"
|
||
size="small"
|
||
color="primary"
|
||
variant="tonal"
|
||
closable
|
||
@click:close="removeTool(toolName)"
|
||
>
|
||
{{ toolName }}
|
||
</v-chip>
|
||
</div>
|
||
<div v-else class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noToolsSelected") }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
|
||
<!-- Skills 选择面板 -->
|
||
<v-expansion-panel value="skills">
|
||
<v-expansion-panel-title>
|
||
<v-icon class="mr-2"> mdi-lightning-bolt </v-icon>
|
||
{{ tm("form.skills") }}
|
||
<v-chip
|
||
v-if="
|
||
Array.isArray(personaForm.skills) &&
|
||
personaForm.skills.length > 0
|
||
"
|
||
size="small"
|
||
color="primary"
|
||
variant="tonal"
|
||
class="ml-2"
|
||
>
|
||
{{ personaForm.skills.length }}
|
||
</v-chip>
|
||
</v-expansion-panel-title>
|
||
|
||
<v-expansion-panel-text>
|
||
<div class="mb-3">
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.skillsHelp") }}
|
||
</p>
|
||
</div>
|
||
|
||
<v-radio-group
|
||
v-model="skillSelectValue"
|
||
class="mt-2"
|
||
hide-details="true"
|
||
>
|
||
<v-radio
|
||
:label="tm('form.skillsAllAvailable')"
|
||
value="0"
|
||
/>
|
||
<v-radio
|
||
:label="tm('form.skillsSelectSpecific')"
|
||
value="1"
|
||
/>
|
||
</v-radio-group>
|
||
|
||
<div
|
||
v-if="skillSelectValue === '1'"
|
||
class="mt-3 selected-config-area"
|
||
>
|
||
<v-text-field
|
||
v-model="skillSearch"
|
||
:label="tm('form.searchSkills')"
|
||
prepend-inner-icon="mdi-magnify"
|
||
variant="outlined"
|
||
density="compact"
|
||
hide-details
|
||
clearable
|
||
class="mb-3"
|
||
/>
|
||
|
||
<div
|
||
v-if="filteredSkills.length > 0"
|
||
class="skills-selection"
|
||
>
|
||
<v-virtual-scroll
|
||
:items="filteredSkills"
|
||
height="240"
|
||
item-height="48"
|
||
>
|
||
<template #default="{ item }">
|
||
<v-list-item
|
||
:key="item.name"
|
||
density="comfortable"
|
||
@click="toggleSkill(item.name)"
|
||
>
|
||
<template #prepend>
|
||
<v-checkbox-btn
|
||
:model-value="isSkillSelected(item.name)"
|
||
@click.stop="toggleSkill(item.name)"
|
||
/>
|
||
</template>
|
||
<v-list-item-title>
|
||
{{ item.name }}
|
||
</v-list-item-title>
|
||
<v-list-item-subtitle v-if="item.description">
|
||
{{ truncateText(item.description, 100) }}
|
||
</v-list-item-subtitle>
|
||
</v-list-item>
|
||
</template>
|
||
</v-virtual-scroll>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="
|
||
!loadingSkills && availableSkills.length === 0
|
||
"
|
||
class="text-center pa-4"
|
||
>
|
||
<v-icon size="48" color="grey-lighten-2" class="mb-2">
|
||
mdi-lightning-bolt
|
||
</v-icon>
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noSkillsAvailable") }}
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="
|
||
!loadingSkills && filteredSkills.length === 0
|
||
"
|
||
class="text-center pa-4"
|
||
>
|
||
<v-icon size="48" color="grey-lighten-2" class="mb-2">
|
||
mdi-magnify
|
||
</v-icon>
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noSkillsFound") }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="loadingSkills" class="text-center pa-4">
|
||
<v-progress-circular indeterminate color="primary" />
|
||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||
{{ tm("form.loadingSkills") }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mt-4">
|
||
<h4 class="text-subtitle-2 mb-2">
|
||
{{ tm("form.selectedSkills") }}
|
||
<span
|
||
v-if="personaForm.skills === null"
|
||
class="text-success"
|
||
>
|
||
({{ tm("form.allSelected") }})
|
||
</span>
|
||
<span v-else-if="Array.isArray(personaForm.skills)">
|
||
({{ personaForm.skills.length }})
|
||
</span>
|
||
</h4>
|
||
<div
|
||
v-if="
|
||
Array.isArray(personaForm.skills) &&
|
||
personaForm.skills.length > 0
|
||
"
|
||
class="d-flex flex-wrap ga-1"
|
||
style="max-height: 100px; overflow-y: auto"
|
||
>
|
||
<v-chip
|
||
v-for="skillName in personaForm.skills"
|
||
:key="skillName"
|
||
size="small"
|
||
color="primary"
|
||
variant="tonal"
|
||
closable
|
||
@click:close="removeSkill(skillName)"
|
||
>
|
||
{{ skillName }}
|
||
</v-chip>
|
||
</div>
|
||
<div v-else class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.noSkillsSelected") }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
|
||
<!-- 预设对话面板 -->
|
||
<v-expansion-panel value="dialogs">
|
||
<v-expansion-panel-title>
|
||
<v-icon class="mr-2"> mdi-chat </v-icon>
|
||
{{ tm("form.presetDialogs") }}
|
||
<v-chip
|
||
v-if="personaForm.begin_dialogs.length > 0"
|
||
size="small"
|
||
color="primary"
|
||
variant="tonal"
|
||
class="ml-2"
|
||
>
|
||
{{ personaForm.begin_dialogs.length / 2 }}
|
||
</v-chip>
|
||
</v-expansion-panel-title>
|
||
|
||
<v-expansion-panel-text>
|
||
<div class="mb-3">
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ tm("form.presetDialogsHelp") }}
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
v-for="(dialog, index) in personaForm.begin_dialogs"
|
||
:key="index"
|
||
class="mb-3"
|
||
>
|
||
<v-textarea
|
||
v-model="personaForm.begin_dialogs[index]"
|
||
:label="
|
||
index % 2 === 0
|
||
? tm('form.userMessage')
|
||
: tm('form.assistantMessage')
|
||
"
|
||
:rules="getDialogRules(index)"
|
||
variant="outlined"
|
||
rows="2"
|
||
density="comfortable"
|
||
>
|
||
<template #append>
|
||
<v-btn
|
||
icon="mdi-delete"
|
||
variant="text"
|
||
size="small"
|
||
color="error"
|
||
@click="removeDialog(index)"
|
||
/>
|
||
</template>
|
||
</v-textarea>
|
||
</div>
|
||
|
||
<v-btn
|
||
variant="outlined"
|
||
prepend-icon="mdi-plus"
|
||
block
|
||
@click="addDialogPair"
|
||
>
|
||
{{ tm("buttons.addDialogPair") }}
|
||
</v-btn>
|
||
</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
</v-expansion-panels>
|
||
</v-col>
|
||
</v-row>
|
||
</v-form>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="persona-form-actions">
|
||
<v-btn
|
||
v-if="editingPersona"
|
||
color="error"
|
||
variant="text"
|
||
@click="deletePersona"
|
||
>
|
||
{{ tm("buttons.delete") }}
|
||
</v-btn>
|
||
<v-spacer />
|
||
<v-btn color="grey" variant="text" @click="closeDialog">
|
||
{{ tm("buttons.cancel") }}
|
||
</v-btn>
|
||
<v-btn
|
||
color="primary"
|
||
variant="flat"
|
||
:loading="saving"
|
||
:disabled="!formValid"
|
||
@click="savePersona"
|
||
>
|
||
{{ tm("buttons.save") }}
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</template>
|
||
|
||
<script>
|
||
import axios from "@/utils/request";
|
||
import { useModuleI18n } from "@/i18n/composables";
|
||
import {
|
||
askForConfirmation as askForConfirmationDialog,
|
||
useConfirmDialog,
|
||
} from "@/utils/confirmDialog";
|
||
|
||
export default {
|
||
name: "PersonaForm",
|
||
props: {
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
editingPersona: {
|
||
type: Object,
|
||
default: null,
|
||
},
|
||
currentFolderId: {
|
||
type: String,
|
||
default: null,
|
||
},
|
||
currentFolderName: {
|
||
type: String,
|
||
default: null,
|
||
},
|
||
},
|
||
emits: ["update:modelValue", "saved", "error", "deleted"],
|
||
setup() {
|
||
const { tm } = useModuleI18n("features/persona");
|
||
const confirmDialog = useConfirmDialog();
|
||
return { tm, confirmDialog };
|
||
},
|
||
data() {
|
||
return {
|
||
toolSelectValue: "0", // 默认选择全部工具
|
||
saving: false,
|
||
expandedPanels: [],
|
||
formValid: false,
|
||
mcpServers: [],
|
||
availableTools: [],
|
||
loadingTools: false,
|
||
availableSkills: [],
|
||
loadingSkills: false,
|
||
existingPersonaIds: [], // 已存在的人格ID列表
|
||
personaForm: {
|
||
persona_id: "",
|
||
system_prompt: "",
|
||
custom_error_message: "",
|
||
begin_dialogs: [],
|
||
tools: [],
|
||
skills: [],
|
||
folder_id: null,
|
||
},
|
||
personaIdRules: [
|
||
(v) => !!v || this.tm("validation.required"),
|
||
(v) =>
|
||
(v && v.length >= 1) || this.tm("validation.minLength", { min: 1 }),
|
||
(v) =>
|
||
!this.existingPersonaIds.includes(v) ||
|
||
this.tm("validation.personaIdExists"),
|
||
],
|
||
systemPromptRules: [
|
||
(v) => !!v || this.tm("validation.required"),
|
||
(v) =>
|
||
(v && v.length >= 10) || this.tm("validation.minLength", { min: 10 }),
|
||
],
|
||
toolSearch: "",
|
||
skillSearch: "",
|
||
skillSelectValue: "0",
|
||
};
|
||
},
|
||
|
||
computed: {
|
||
showDialog: {
|
||
get() {
|
||
return this.modelValue;
|
||
},
|
||
set(value) {
|
||
this.$emit("update:modelValue", value);
|
||
},
|
||
},
|
||
filteredTools() {
|
||
if (!this.toolSearch) {
|
||
return this.availableTools;
|
||
}
|
||
const search = this.toolSearch.toLowerCase();
|
||
return this.availableTools.filter(
|
||
(tool) =>
|
||
tool.name.toLowerCase().includes(search) ||
|
||
(tool.description &&
|
||
tool.description.toLowerCase().includes(search)) ||
|
||
(tool.mcp_server_name &&
|
||
tool.mcp_server_name.toLowerCase().includes(search)),
|
||
);
|
||
},
|
||
filteredSkills() {
|
||
if (!this.skillSearch) {
|
||
return this.availableSkills;
|
||
}
|
||
const search = this.skillSearch.toLowerCase();
|
||
return this.availableSkills.filter(
|
||
(skill) =>
|
||
skill.name.toLowerCase().includes(search) ||
|
||
(skill.description &&
|
||
skill.description.toLowerCase().includes(search)),
|
||
);
|
||
},
|
||
folderDisplayName() {
|
||
// 优先使用传入的文件夹名称
|
||
if (this.currentFolderName) {
|
||
return this.currentFolderName;
|
||
}
|
||
// 如果没有文件夹 ID,显示根目录
|
||
if (!this.currentFolderId) {
|
||
return this.tm("form.rootFolder");
|
||
}
|
||
// 否则显示文件夹 ID(作为备用)
|
||
return this.currentFolderId;
|
||
},
|
||
},
|
||
|
||
watch: {
|
||
modelValue(newValue) {
|
||
if (newValue) {
|
||
// 只有在不是编辑状态时才初始化空表单
|
||
if (this.editingPersona) {
|
||
this.initFormWithPersona(this.editingPersona);
|
||
} else {
|
||
this.initForm();
|
||
// 只在创建新人格时加载已存在的人格列表
|
||
this.loadExistingPersonaIds();
|
||
}
|
||
this.loadMcpServers();
|
||
this.loadTools();
|
||
this.loadSkills();
|
||
}
|
||
},
|
||
editingPersona: {
|
||
immediate: true,
|
||
handler(newPersona) {
|
||
// 只有在对话框打开时才处理editingPersona的变化
|
||
if (this.modelValue) {
|
||
if (newPersona) {
|
||
this.initFormWithPersona(newPersona);
|
||
} else {
|
||
this.initForm();
|
||
}
|
||
}
|
||
},
|
||
},
|
||
toolSelectValue(newValue) {
|
||
if (newValue === "0") {
|
||
// 选择全部工具
|
||
this.personaForm.tools = null;
|
||
} else if (newValue === "1") {
|
||
// 选择指定工具,如果当前是null,则转换为空数组
|
||
if (this.personaForm.tools === null) {
|
||
this.personaForm.tools = [];
|
||
}
|
||
}
|
||
},
|
||
skillSelectValue(newValue) {
|
||
if (newValue === "0") {
|
||
this.personaForm.skills = null;
|
||
} else if (newValue === "1") {
|
||
if (this.personaForm.skills === null) {
|
||
this.personaForm.skills = [];
|
||
}
|
||
}
|
||
},
|
||
},
|
||
|
||
methods: {
|
||
initForm() {
|
||
this.personaForm = {
|
||
persona_id: "",
|
||
system_prompt: "",
|
||
custom_error_message: "",
|
||
begin_dialogs: [],
|
||
tools: [],
|
||
skills: [],
|
||
folder_id: this.currentFolderId,
|
||
};
|
||
this.toolSelectValue = "0";
|
||
this.skillSelectValue = "0";
|
||
this.expandedPanels = this.getDefaultExpandedPanels();
|
||
},
|
||
|
||
initFormWithPersona(persona) {
|
||
this.personaForm = {
|
||
persona_id: persona.persona_id,
|
||
system_prompt: persona.system_prompt,
|
||
custom_error_message: persona.custom_error_message || "",
|
||
begin_dialogs: [...(persona.begin_dialogs || [])],
|
||
tools: persona.tools === null ? null : [...(persona.tools || [])],
|
||
skills: persona.skills === null ? null : [...(persona.skills || [])],
|
||
folder_id: persona.folder_id,
|
||
};
|
||
// 根据 tools 的值设置 toolSelectValue
|
||
this.toolSelectValue = persona.tools === null ? "0" : "1";
|
||
this.skillSelectValue = persona.skills === null ? "0" : "1";
|
||
this.expandedPanels = this.getDefaultExpandedPanels();
|
||
},
|
||
|
||
getDefaultExpandedPanels() {
|
||
return this.$vuetify.display.smAndDown
|
||
? []
|
||
: ["tools", "skills", "dialogs"];
|
||
},
|
||
|
||
closeDialog() {
|
||
this.showDialog = false;
|
||
},
|
||
|
||
async loadMcpServers() {
|
||
try {
|
||
const response = await axios.get("/api/tools/mcp/servers");
|
||
if (response.data.status === "ok") {
|
||
this.mcpServers = response.data.data || [];
|
||
} else {
|
||
this.$emit(
|
||
"error",
|
||
response.data.message || "Failed to load MCP servers",
|
||
);
|
||
}
|
||
} catch (error) {
|
||
this.$emit(
|
||
"error",
|
||
error.response?.data?.message || "Failed to load MCP servers",
|
||
);
|
||
this.mcpServers = [];
|
||
}
|
||
},
|
||
|
||
async loadTools() {
|
||
this.loadingTools = true;
|
||
try {
|
||
const response = await axios.get("/api/tools/list");
|
||
if (response.data.status === "ok") {
|
||
this.availableTools = response.data.data || [];
|
||
} else {
|
||
this.$emit("error", response.data.message || "Failed to load tools");
|
||
}
|
||
} catch (error) {
|
||
this.$emit(
|
||
"error",
|
||
error.response?.data?.message || "Failed to load tools",
|
||
);
|
||
this.availableTools = [];
|
||
} finally {
|
||
this.loadingTools = false;
|
||
}
|
||
},
|
||
|
||
async loadSkills() {
|
||
this.loadingSkills = true;
|
||
try {
|
||
const response = await axios.get("/api/skills");
|
||
if (response.data.status === "ok") {
|
||
const payload = response.data.data || [];
|
||
if (Array.isArray(payload)) {
|
||
this.availableSkills = payload.filter(
|
||
(skill) => skill.active !== false,
|
||
);
|
||
} else {
|
||
const skills = payload.skills || [];
|
||
this.availableSkills = skills.filter(
|
||
(skill) => skill.active !== false,
|
||
);
|
||
}
|
||
} else {
|
||
this.$emit("error", response.data.message || "Failed to load skills");
|
||
}
|
||
} catch (error) {
|
||
this.$emit(
|
||
"error",
|
||
error.response?.data?.message || "Failed to load skills",
|
||
);
|
||
this.availableSkills = [];
|
||
} finally {
|
||
this.loadingSkills = false;
|
||
}
|
||
},
|
||
|
||
async loadExistingPersonaIds() {
|
||
try {
|
||
const response = await axios.get("/api/persona/list");
|
||
if (response.data.status === "ok") {
|
||
this.existingPersonaIds = (response.data.data || []).map(
|
||
(p) => p.persona_id,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
// 加载失败不影响表单使用,只是无法进行前端重名校验
|
||
this.existingPersonaIds = [];
|
||
}
|
||
},
|
||
|
||
async savePersona() {
|
||
if (!this.formValid) return;
|
||
|
||
// 验证预设对话不能为空
|
||
if (this.personaForm.begin_dialogs.length > 0) {
|
||
for (let i = 0; i < this.personaForm.begin_dialogs.length; i++) {
|
||
if (
|
||
!this.personaForm.begin_dialogs[i] ||
|
||
this.personaForm.begin_dialogs[i].trim() === ""
|
||
) {
|
||
const dialogType =
|
||
i % 2 === 0
|
||
? this.tm("form.userMessage")
|
||
: this.tm("form.assistantMessage");
|
||
this.$emit(
|
||
"error",
|
||
this.tm("validation.dialogRequired", { type: dialogType }),
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
this.saving = true;
|
||
try {
|
||
const url = this.editingPersona
|
||
? "/api/persona/update"
|
||
: "/api/persona/create";
|
||
|
||
// 白名单过滤字段
|
||
const allowedFields = [
|
||
"persona_id",
|
||
"system_prompt",
|
||
"begin_dialogs",
|
||
"tools",
|
||
];
|
||
const filteredData = {};
|
||
allowedFields.forEach((field) => {
|
||
if (this.personaForm.hasOwnProperty(field)) {
|
||
filteredData[field] = this.personaForm[field];
|
||
}
|
||
});
|
||
|
||
const response = await axios.post(url, filteredData);
|
||
|
||
if (response.data.status === "ok") {
|
||
this.$emit(
|
||
"saved",
|
||
response.data.message || this.tm("messages.saveSuccess"),
|
||
);
|
||
this.closeDialog();
|
||
} else {
|
||
this.$emit(
|
||
"error",
|
||
response.data.message || this.tm("messages.saveError"),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
this.$emit(
|
||
"error",
|
||
error.response?.data?.message || this.tm("messages.saveError"),
|
||
);
|
||
}
|
||
this.saving = false;
|
||
},
|
||
|
||
async deletePersona() {
|
||
if (!this.editingPersona) return;
|
||
|
||
if (
|
||
!(await askForConfirmationDialog(
|
||
this.tm("messages.deleteConfirm", {
|
||
id: this.editingPersona.persona_id,
|
||
}),
|
||
this.confirmDialog,
|
||
))
|
||
) {
|
||
return;
|
||
}
|
||
|
||
this.saving = true;
|
||
try {
|
||
const response = await axios.post("/api/persona/delete", {
|
||
persona_id: this.editingPersona.persona_id,
|
||
});
|
||
|
||
if (response.data.status === "ok") {
|
||
this.$emit(
|
||
"deleted",
|
||
response.data.message || this.tm("messages.deleteSuccess"),
|
||
);
|
||
this.closeDialog();
|
||
} else {
|
||
this.$emit(
|
||
"error",
|
||
response.data.message || this.tm("messages.deleteError"),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
this.$emit(
|
||
"error",
|
||
error.response?.data?.message || this.tm("messages.deleteError"),
|
||
);
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
|
||
addDialogPair() {
|
||
this.personaForm.begin_dialogs.push("", "");
|
||
// 自动展开预设对话面板
|
||
if (!this.expandedPanels.includes("dialogs")) {
|
||
this.expandedPanels.push("dialogs");
|
||
}
|
||
},
|
||
|
||
removeDialog(index) {
|
||
// 如果是偶数索引(用户消息),删除用户消息和对应的助手消息
|
||
if (
|
||
index % 2 === 0 &&
|
||
index + 1 < this.personaForm.begin_dialogs.length
|
||
) {
|
||
this.personaForm.begin_dialogs.splice(index, 2);
|
||
}
|
||
// 如果是奇数索引(助手消息),删除助手消息和对应的用户消息
|
||
else if (index % 2 === 1 && index - 1 >= 0) {
|
||
this.personaForm.begin_dialogs.splice(index - 1, 2);
|
||
}
|
||
},
|
||
|
||
toggleMcpServer(server) {
|
||
if (!server.tools || server.tools.length === 0) return;
|
||
|
||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||
if (this.personaForm.tools === null) {
|
||
// 从全选状态转换为去除该服务器工具的状态
|
||
this.personaForm.tools = this.availableTools
|
||
.map((tool) => tool.name)
|
||
.filter((toolName) => !server.tools.includes(toolName));
|
||
this.toolSelectValue = "1"; // 切换到指定工具模式
|
||
return;
|
||
}
|
||
|
||
// 确保tools是数组
|
||
if (!Array.isArray(this.personaForm.tools)) {
|
||
this.personaForm.tools = [];
|
||
this.toolSelectValue = "1";
|
||
}
|
||
|
||
// 检查是否所有服务器的工具都已选中
|
||
const serverTools = server.tools;
|
||
const allSelected = serverTools.every((toolName) =>
|
||
this.personaForm.tools.includes(toolName),
|
||
);
|
||
|
||
if (allSelected) {
|
||
// 移除所有服务器工具
|
||
this.personaForm.tools = this.personaForm.tools.filter(
|
||
(toolName) => !serverTools.includes(toolName),
|
||
);
|
||
} else {
|
||
// 添加所有服务器工具
|
||
serverTools.forEach((toolName) => {
|
||
if (!this.personaForm.tools.includes(toolName)) {
|
||
this.personaForm.tools.push(toolName);
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
toggleTool(toolName) {
|
||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||
if (this.personaForm.tools === null) {
|
||
// 如果是全选状态,点击某个工具表示要取消选择该工具
|
||
// 所以创建一个包含所有其他工具的数组
|
||
this.personaForm.tools = this.availableTools
|
||
.map((tool) => tool.name)
|
||
.filter((name) => name !== toolName);
|
||
this.toolSelectValue = "1"; // 切换到指定工具模式
|
||
} else if (Array.isArray(this.personaForm.tools)) {
|
||
const index = this.personaForm.tools.indexOf(toolName);
|
||
if (index !== -1) {
|
||
// 如果工具已选择,移除工具
|
||
this.personaForm.tools.splice(index, 1);
|
||
} else {
|
||
// 如果工具未选择,添加工具
|
||
this.personaForm.tools.push(toolName);
|
||
}
|
||
} else {
|
||
// 如果tools不是数组也不是null,初始化为数组
|
||
this.personaForm.tools = [toolName];
|
||
this.toolSelectValue = "1";
|
||
}
|
||
},
|
||
|
||
removeTool(toolName) {
|
||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||
if (this.personaForm.tools === null) {
|
||
// 创建一个包含所有工具的数组,然后移除指定工具
|
||
this.personaForm.tools = this.availableTools
|
||
.map((tool) => tool.name)
|
||
.filter((name) => name !== toolName);
|
||
this.toolSelectValue = "1"; // 切换到指定工具模式
|
||
} else if (Array.isArray(this.personaForm.tools)) {
|
||
const index = this.personaForm.tools.indexOf(toolName);
|
||
if (index !== -1) {
|
||
this.personaForm.tools.splice(index, 1);
|
||
}
|
||
}
|
||
},
|
||
|
||
toggleSkill(skillName) {
|
||
if (this.personaForm.skills === null) {
|
||
this.personaForm.skills = this.availableSkills
|
||
.map((skill) => skill.name)
|
||
.filter((name) => name !== skillName);
|
||
this.skillSelectValue = "1";
|
||
} else if (Array.isArray(this.personaForm.skills)) {
|
||
const index = this.personaForm.skills.indexOf(skillName);
|
||
if (index !== -1) {
|
||
this.personaForm.skills.splice(index, 1);
|
||
} else {
|
||
this.personaForm.skills.push(skillName);
|
||
}
|
||
} else {
|
||
this.personaForm.skills = [skillName];
|
||
this.skillSelectValue = "1";
|
||
}
|
||
},
|
||
|
||
removeSkill(skillName) {
|
||
if (this.personaForm.skills === null) {
|
||
this.personaForm.skills = this.availableSkills
|
||
.map((skill) => skill.name)
|
||
.filter((name) => name !== skillName);
|
||
this.skillSelectValue = "1";
|
||
} else if (Array.isArray(this.personaForm.skills)) {
|
||
const index = this.personaForm.skills.indexOf(skillName);
|
||
if (index !== -1) {
|
||
this.personaForm.skills.splice(index, 1);
|
||
}
|
||
}
|
||
},
|
||
|
||
truncateText(text, maxLength) {
|
||
if (!text) return "";
|
||
return text.length > maxLength
|
||
? text.substring(0, maxLength) + "..."
|
||
: text;
|
||
},
|
||
|
||
getDialogRules(index) {
|
||
const dialogType =
|
||
index % 2 === 0
|
||
? this.tm("form.userMessage")
|
||
: this.tm("form.assistantMessage");
|
||
return [
|
||
(v) =>
|
||
!!v || this.tm("validation.dialogRequired", { type: dialogType }),
|
||
(v) =>
|
||
(v && v.trim().length > 0) ||
|
||
this.tm("validation.dialogRequired", { type: dialogType }),
|
||
];
|
||
},
|
||
|
||
isToolSelected(toolName) {
|
||
// 如果是全选状态,所有工具都被选中
|
||
if (this.personaForm.tools === null) {
|
||
return true;
|
||
}
|
||
return (
|
||
Array.isArray(this.personaForm.tools) &&
|
||
this.personaForm.tools.includes(toolName)
|
||
);
|
||
},
|
||
|
||
isSkillSelected(skillName) {
|
||
if (this.personaForm.skills === null) {
|
||
return true;
|
||
}
|
||
return (
|
||
Array.isArray(this.personaForm.skills) &&
|
||
this.personaForm.skills.includes(skillName)
|
||
);
|
||
},
|
||
|
||
isServerSelected(server) {
|
||
if (!server.tools || server.tools.length === 0) return false;
|
||
|
||
// 如果是全选状态,所有服务器都被选中
|
||
if (this.personaForm.tools === null) {
|
||
return true;
|
||
}
|
||
|
||
// 检查服务器的所有工具是否都已选中
|
||
return (
|
||
Array.isArray(this.personaForm.tools) &&
|
||
server.tools.every((toolName) =>
|
||
this.personaForm.tools.includes(toolName),
|
||
)
|
||
);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.persona-form-card {
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.persona-form-content {
|
||
max-height: min(78vh, 760px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.persona-form-title {
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.persona-form-actions {
|
||
position: sticky;
|
||
bottom: 0;
|
||
z-index: 2;
|
||
background: rgb(var(--v-theme-surface));
|
||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||
}
|
||
|
||
.selected-config-area {
|
||
margin-left: 32px;
|
||
}
|
||
|
||
.persona-form-layout {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.tools-selection {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.skills-selection {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.v-virtual-scroll {
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.persona-form-card-mobile {
|
||
border-radius: 0;
|
||
}
|
||
|
||
.persona-form-content {
|
||
max-height: calc(100vh - 128px);
|
||
padding: 16px !important;
|
||
}
|
||
|
||
.persona-basic-col,
|
||
.persona-panels-col {
|
||
padding-top: 0 !important;
|
||
}
|
||
|
||
.persona-form-title {
|
||
font-size: 1.15rem !important;
|
||
padding: 12px 16px !important;
|
||
}
|
||
|
||
.selected-config-area {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.tools-selection,
|
||
.skills-selection {
|
||
max-height: 38vh;
|
||
}
|
||
|
||
.persona-form-actions {
|
||
padding: 12px 16px !important;
|
||
gap: 8px;
|
||
}
|
||
|
||
.persona-form-actions .v-btn {
|
||
min-width: 0;
|
||
}
|
||
}
|
||
</style>
|