Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 (#2328)

* refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离

* Refactor: 重构配置前端页面,新增数个配置项 (#2331)

* refactor: 重构配置前端页面,新增数个配置项

* feat: 完善多配置文件结构

* perf: 系统配置入口

* fix: normal config item list not display

* fix: 修复 axios 请求中的上下文引用问题
This commit is contained in:
Soulter
2025-08-13 09:18:49 +08:00
committed by GitHub
parent 6c1f540170
commit 369eab18ab
31 changed files with 2611 additions and 786 deletions
@@ -176,7 +176,7 @@ function saveEditedContent() {
<!-- List item -->
<ListConfigItem
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
:value="iterable[key]"
v-model="iterable[key]"
class="config-field"
/>
</div>
@@ -287,9 +287,9 @@ function saveEditedContent() {
></v-switch>
<!-- List item -->
<ListConfigItem
<ListConfigItem
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
:value="iterable[metadataKey]"
v-model="iterable[metadataKey]"
class="config-field"
/>
</div>
@@ -0,0 +1,396 @@
<script setup>
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { ref, computed } from 'vue'
import ListConfigItem from './ListConfigItem.vue'
import ProviderSelector from './ProviderSelector.vue'
import PersonaSelector from './PersonaSelector.vue'
import { useI18n } from '@/i18n/composables'
const props = defineProps({
metadata: {
type: Object,
required: true
},
iterable: {
type: Object,
required: true
},
metadataKey: {
type: String,
required: true
}
})
const { t } = useI18n()
const dialog = ref(false)
const currentEditingKey = ref('')
const currentEditingLanguage = ref('json')
const currentEditingTheme = ref('vs-light')
let currentEditingKeyIterable = null
function getValueBySelector(obj, selector) {
const keys = selector.split('.')
let current = obj
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key]
} else {
return undefined
}
}
return current
}
function setValueBySelector(obj, selector, value) {
const keys = selector.split('.')
let current = obj
// 创建嵌套对象路径
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i]
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {}
}
current = current[key]
}
// 设置最终值
current[keys[keys.length - 1]] = value
}
// 创建一个计算属性来处理 JSON selector 的获取和设置
function createSelectorModel(selector) {
return computed({
get() {
return getValueBySelector(props.iterable, selector)
},
set(value) {
setValueBySelector(props.iterable, selector, value)
}
})
}
function openEditorDialog(key, value, theme, language) {
currentEditingKey.value = key
currentEditingLanguage.value = language || 'json'
currentEditingTheme.value = theme || 'vs-light'
currentEditingKeyIterable = value
dialog.value = true
}
function saveEditedContent() {
dialog.value = false
}
function shouldShowItem(itemMeta, itemKey) {
if (!itemMeta?.condition) {
return true
}
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
const actualValue = getValueBySelector(props.iterable, conditionKey)
if (actualValue !== expectedValue) {
return false
}
}
return true
}
function hasVisibleItemsAfter(items, currentIndex) {
const itemEntries = Object.entries(items)
// 检查当前索引之后是否还有可见的配置项
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
const [itemKey, itemMeta] = itemEntries[i]
if (shouldShowItem(itemMeta, itemKey)) {
return true
}
}
return false
}
</script>
<template>
<v-card style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));" rounded="md" variant="outlined">
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'">
<v-list-item-title class="config-title">
{{ metadata[metadataKey]?.description }}
</v-list-item-title>
<v-list-item-subtitle class="config-hint">
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint"></span>
{{ metadata[metadataKey]?.hint }}
</v-list-item-subtitle>
</v-card-text>
<!-- Object Type Configuration with JSON Selector Support -->
<div v-if="metadata[metadataKey]?.type === 'object'" class="object-config">
<div v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items" :key="itemKey" class="config-item">
<!-- Check if itemKey is a JSON selector -->
<template v-if="shouldShowItem(itemMeta, itemKey)">
<!-- JSON Selector Property -->
<v-row v-if="!itemMeta?.invisible" class="config-row">
<v-col cols="12" sm="6" class="property-info">
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ itemMeta?.description || itemKey }}
<span class="property-key">({{ itemKey }})</span>
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint"></span>
{{ itemMeta?.hint }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="6" class="config-input">
<div class="w-100" v-if="!itemMeta?._special">
<!-- Select input for JSON selector -->
<v-select v-if="itemMeta?.options" v-model="createSelectorModel(itemKey).value"
:items="itemMeta?.options" :disabled="itemMeta?.readonly" density="compact" variant="outlined"
class="config-field" hide-details></v-select>
<!-- Code Editor for JSON selector -->
<div v-else-if="itemMeta?.editor_mode" class="editor-container">
<VueMonacoEditor :theme="itemMeta?.editor_theme || 'vs-light'"
:language="itemMeta?.editor_language || 'json'"
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
v-model:value="createSelectorModel(itemKey).value">
</VueMonacoEditor>
<v-btn icon size="small" variant="text" color="primary" class="editor-fullscreen-btn"
@click="openEditorDialog(itemKey, iterable, itemMeta?.editor_theme, itemMeta?.editor_language)"
:title="t('core.common.editor.fullscreen')">
<v-icon>mdi-fullscreen</v-icon>
</v-btn>
</div>
<!-- String input for JSON selector -->
<v-text-field v-else-if="itemMeta?.type === 'string'" v-model="createSelectorModel(itemKey).value"
density="compact" variant="outlined" class="config-field" hide-details></v-text-field>
<!-- Numeric input for JSON selector -->
<v-text-field v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined" class="config-field"
type="number" hide-details></v-text-field>
<!-- Text area for JSON selector -->
<v-textarea v-else-if="itemMeta?.type === 'text'" v-model="createSelectorModel(itemKey).value"
variant="outlined" rows="3" class="config-field" hide-details></v-textarea>
<!-- Boolean switch for JSON selector -->
<v-switch v-else-if="itemMeta?.type === 'bool'" v-model="createSelectorModel(itemKey).value"
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
<!-- List item for JSON selector -->
<ListConfigItem
v-else-if="itemMeta?.type === 'list'"
v-model="createSelectorModel(itemKey).value"
button-text="修改"
class="config-field"
/>
<!-- Fallback for JSON selector -->
<v-text-field v-else v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined"
class="config-field" hide-details></v-text-field>
</div>
<div v-else-if="itemMeta?._special === 'select_provider'">
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'chat_completion'"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'speech_to_text'"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'text_to_speech'"
/>
</div>
<div v-else-if="itemMeta?._special === 'provider_pool'">
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'chat_completion'"
button-text="选择提供商池..."
/>
</div>
<div v-else-if="itemMeta?._special === 'select_persona'">
<PersonaSelector
v-model="createSelectorModel(itemKey).value"
/>
</div>
<div v-else-if="itemMeta?._special === 'persona_pool'">
<PersonaSelector
v-model="createSelectorModel(itemKey).value"
button-text="选择人格池..."
/>
</div>
</v-col>
</v-row>
</template>
<v-divider class="config-divider" v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
</div>
</div>
</v-card>
<!-- Full Screen Editor Dialog -->
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
<VueMonacoEditor :theme="currentEditingTheme" :language="currentEditingLanguage"
style="height: calc(100vh - 64px);" v-model:value="currentEditingKeyIterable[currentEditingKey]">
</VueMonacoEditor>
</v-card-text>
</v-card>
</v-dialog>
</template>
<style scoped>
.config-section {
margin-bottom: 4px;
}
.config-title {
/* font-weight: 600; */
font-size: 1.3rem;
color: var(--v-theme-primaryText);
}
.config-hint {
font-size: 0.75rem;
color: var(--v-theme-secondaryText);
margin-top: 2px;
}
.metadata-key,
.property-key {
font-size: 0.85em;
opacity: 0.7;
font-weight: normal;
display: none;
}
.important-hint {
opacity: 1;
margin-right: 4px;
}
.object-config,
.simple-config {
width: 100%;
}
.nested-object {
padding-left: 16px;
}
.nested-container {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 12px;
margin: 12px 0;
background-color: rgba(0, 0, 0, 0.02);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.config-row {
margin: 0;
align-items: center;
padding: 10px 8px;
border-radius: 4px;
}
.config-row:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.property-info {
padding: 0;
}
.property-name {
font-size: 0.875rem;
/* font-weight: 600; */
color: var(--v-theme-primaryText);
}
.property-hint {
font-size: 0.75rem;
color: var(--v-theme-secondaryText);
margin-top: 2px;
}
.type-indicator {
display: flex;
justify-content: center;
}
.config-input {
padding: 4px 8px;
}
.config-field {
margin-bottom: 0;
}
.config-divider {
border-color: rgba(0, 0, 0, 0.1);
margin-left: 24px;
}
.editor-container {
position: relative;
display: flex;
width: 100%;
}
.editor-fullscreen-btn {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.editor-fullscreen-btn:hover {
background-color: rgba(0, 0, 0, 0.5);
}
@media (max-width: 600px) {
.nested-object {
padding-left: 8px;
}
.config-row {
padding: 8px 0;
}
.property-info,
.type-indicator,
.config-input {
padding: 4px;
}
}
</style>
+194 -113
View File
@@ -1,135 +1,216 @@
<template>
<div class="list-config-item">
<v-list dense style="background-color: transparent;max-height: 300px; overflow-y: auto;">
<v-list-item v-for="(item, index) in items" :key="index">
<v-list-item-content style="display: flex; justify-content: space-between;">
<v-list-item-title v-if="editIndex !== index">
<v-chip size="small" label color="primary">{{ item }}</v-chip>
</v-list-item-title>
<div class="d-flex align-center justify-space-between">
<div>
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
暂无项目
</span>
<div v-else class="d-flex flex-wrap ga-2">
<v-chip v-for="item in displayItems" :key="item" size="x-small" label color="primary">
{{ item.length > 20 ? item.slice(0, 20) + '...' : item }}
</v-chip>
<v-chip v-if="modelValue.length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
+{{ modelValue.length - maxDisplayItems }}
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
</v-btn>
</div>
<!-- List Management Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
{{ dialogTitle }}
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-list v-if="localItems.length > 0" density="compact">
<v-list-item
v-for="(item, index) in localItems"
:key="index"
rounded="md"
class="ma-1">
<v-list-item-title v-if="editIndex !== index">
{{ item }}
</v-list-item-title>
<v-text-field
v-else
v-model="editItem"
hide-details
variant="outlined"
density="compact"
@keyup.enter="saveEdit"
@keyup.esc="cancelEdit"
autofocus
></v-text-field>
<template v-slot:append>
<div v-if="editIndex !== index" class="d-flex">
<v-btn @click="startEdit(index, item)" variant="plain" icon size="small">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn @click="removeItem(index)" variant="plain" icon size="small">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div v-else class="d-flex">
<v-btn @click="saveEdit" variant="plain" color="success" icon size="small">
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn @click="cancelEdit" variant="plain" color="error" icon size="small">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-format-list-bulleted</v-icon>
<p class="text-grey mt-4">暂无项目</p>
</div>
</v-card-text>
<!-- Add new item section -->
<v-card-text class="pa-4">
<div class="d-flex align-center ga-2">
<v-text-field
v-else
v-model="editItem"
dense
hide-details
v-model="newItem"
:label="t('core.common.list.addItemPlaceholder')"
@keyup.enter="addItem"
clearable
hide-details
variant="outlined"
density="compact"
@keyup.enter="saveEdit"
@keyup.esc="cancelEdit"
autofocus
></v-text-field>
<div v-if="editIndex !== index">
<v-btn @click="startEdit(index, item)" variant="plain" class="edit-btn" icon size="small">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn @click="removeItem(index)" variant="plain" icon size="small">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div v-else>
<v-btn @click="saveEdit" variant="plain" color="success" icon size="small">
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn @click="cancelEdit" variant="plain" color="error" icon size="small">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-list-item-content>
</v-list-item>
</v-list>
<div style="display: flex; align-items: center;">
<v-text-field v-model="newItem" :label="t('core.common.list.addItemPlaceholder')" @keyup.enter="addItem" clearable dense hide-details
variant="outlined" density="compact"></v-text-field>
<v-btn @click="addItem" text variant="tonal">
<v-icon>mdi-plus</v-icon>
{{ t('core.common.list.addButton') }}
</v-btn>
</div>
class="flex-grow-1">
</v-text-field>
<v-btn @click="addItem" variant="tonal" color="primary">
<v-icon>mdi-plus</v-icon>
{{ t('core.common.list.addButton') }}
</v-btn>
</div>
</v-card-text>
</div>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelDialog">取消</v-btn>
<v-btn color="primary" @click="confirmDialog">确认</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { useI18n } from '@/i18n/composables';
<script setup>
import { ref, computed, watch } from 'vue'
import { useI18n } from '@/i18n/composables'
export default {
name: 'ListConfigItem',
setup() {
const { t } = useI18n();
return { t };
const { t } = useI18n()
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
props: {
value: {
type: Array,
default: () => [],
},
label: {
type: String,
default: '',
},
label: {
type: String,
default: ''
},
data() {
return {
newItem: '',
items: this.value,
editIndex: -1,
editItem: '',
};
buttonText: {
type: String,
default: '修改'
},
watch: {
items(newVal) {
this.$emit('input', newVal);
},
dialogTitle: {
type: String,
default: '修改列表项'
},
methods: {
addItem() {
if (this.newItem.trim() !== '') {
this.items.push(this.newItem.trim());
this.newItem = '';
}
},
removeItem(index) {
this.items.splice(index, 1);
},
startEdit(index, item) {
this.editIndex = index;
this.editItem = item;
},
saveEdit() {
if (this.editItem.trim() !== '') {
this.items[this.editIndex] = this.editItem.trim();
this.cancelEdit();
}
},
cancelEdit() {
this.editIndex = -1;
this.editItem = '';
},
},
};
maxDisplayItems: {
type: Number,
default: 1
}
})
const emit = defineEmits(['update:modelValue'])
const dialog = ref(false)
const localItems = ref([])
const originalItems = ref([])
const newItem = ref('')
const editIndex = ref(-1)
const editItem = ref('')
// 计算要显示的项目
const displayItems = computed(() => {
return props.modelValue.slice(0, props.maxDisplayItems)
})
// 监听 modelValue 变化,同步到 localItems
watch(() => props.modelValue, (newValue) => {
localItems.value = [...(newValue || [])]
}, { immediate: true })
function openDialog() {
localItems.value = [...(props.modelValue || [])]
originalItems.value = [...(props.modelValue || [])]
dialog.value = true
editIndex.value = -1
editItem.value = ''
newItem.value = ''
}
function addItem() {
if (newItem.value.trim() !== '') {
localItems.value.push(newItem.value.trim())
newItem.value = ''
}
}
function removeItem(index) {
localItems.value.splice(index, 1)
}
function startEdit(index, item) {
editIndex.value = index
editItem.value = item
}
function saveEdit() {
if (editItem.value.trim() !== '') {
localItems.value[editIndex.value] = editItem.value.trim()
cancelEdit()
}
}
function cancelEdit() {
editIndex.value = -1
editItem.value = ''
}
function confirmDialog() {
emit('update:modelValue', [...localItems.value])
dialog.value = false
}
function cancelDialog() {
localItems.value = [...originalItems.value]
editIndex.value = -1
editItem.value = ''
newItem.value = ''
dialog.value = false
}
</script>
<style scoped>
.list-config-item {
border: 1px solid var(--v-theme-border);
padding: 16px;
margin-bottom: 8px;
border-radius: 10px;
background-color: var(--v-theme-background);
}
.v-list-item {
padding: 0;
transition: all 0.2s ease;
}
.v-list-item-title {
font-size: 14px;
.v-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
}
.v-btn {
margin-left: 8px;
}
.edit-btn {
margin-right: -8px;
.v-chip {
margin: 2px;
}
</style>
@@ -0,0 +1,141 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
未选择
</span>
<span v-else>
{{ modelValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
</v-btn>
</div>
<!-- Persona Selection Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择人格
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<v-list v-if="!loading && personaList.length > 0" density="compact">
<v-list-item
v-for="persona in personaList"
:key="persona.persona_id"
:value="persona.persona_id"
@click="selectPersona(persona)"
:active="selectedPersona === persona.persona_id"
rounded="md"
class="ma-1">
<v-list-item-title>{{ persona.persona_id }}</v-list-item-title>
<v-list-item-subtitle>
{{ persona.system_prompt ? persona.system_prompt.substring(0, 50) + '...' : '无描述' }}
</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="selectedPersona === persona.persona_id" color="primary">mdi-check-circle</v-icon>
</template>
</v-list-item>
</v-list>
<div v-else-if="!loading && personaList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-account-off</v-icon>
<p class="text-grey mt-4">暂无可用的人格</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn
color="primary"
@click="confirmSelection"
:disabled="!selectedPersona">
确认选择
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import axios from 'axios'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
buttonText: {
type: String,
default: '选择人格...'
}
})
const emit = defineEmits(['update:modelValue'])
const dialog = ref(false)
const personaList = ref([])
const loading = ref(false)
const selectedPersona = ref('')
// 监听 modelValue 变化,同步到 selectedPersona
watch(() => props.modelValue, (newValue) => {
selectedPersona.value = newValue || ''
}, { immediate: true })
async function openDialog() {
selectedPersona.value = props.modelValue || ''
dialog.value = true
await loadPersonas()
}
async function loadPersonas() {
loading.value = true
try {
const response = await axios.get('/api/persona/list')
if (response.data.status === 'ok') {
personaList.value = response.data.data || []
}
} catch (error) {
console.error('加载人格列表失败:', error)
personaList.value = []
} finally {
loading.value = false
}
}
function selectPersona(persona) {
selectedPersona.value = persona.persona_id
}
function confirmSelection() {
emit('update:modelValue', selectedPersona.value)
dialog.value = false
}
function cancelSelection() {
selectedPersona.value = props.modelValue || ''
dialog.value = false
}
</script>
<style scoped>
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
}
.v-list-item.v-list-item--active {
background-color: rgba(var(--v-theme-primary), 0.08);
}
</style>
@@ -0,0 +1,150 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
未选择
</span>
<span v-else>
{{ modelValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
</v-btn>
</div>
<!-- Provider Selection Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择提供商
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<v-list v-if="!loading && providerList.length > 0" density="compact">
<v-list-item
v-for="provider in providerList"
:key="provider.id"
:value="provider.id"
@click="selectProvider(provider)"
:active="selectedProvider === provider.id"
rounded="md"
class="ma-1">
<v-list-item-title>{{ provider.id }}</v-list-item-title>
<v-list-item-subtitle>
{{ provider.type || provider.provider_type || '未知类型' }}
<span v-if="provider.model_config?.model">- {{ provider.model_config.model }}</span>
</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="selectedProvider === provider.id" color="primary">mdi-check-circle</v-icon>
</template>
</v-list-item>
</v-list>
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
<p class="text-grey mt-4">暂无可用的提供商</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn
color="primary"
@click="confirmSelection"
:disabled="!selectedProvider">
确认选择
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import axios from 'axios'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
providerType: {
type: String,
default: 'chat_completion'
},
buttonText: {
type: String,
default: '选择提供商...'
}
})
const emit = defineEmits(['update:modelValue'])
const dialog = ref(false)
const providerList = ref([])
const loading = ref(false)
const selectedProvider = ref('')
// 监听 modelValue 变化,同步到 selectedProvider
watch(() => props.modelValue, (newValue) => {
selectedProvider.value = newValue || ''
}, { immediate: true })
async function openDialog() {
selectedProvider.value = props.modelValue || ''
dialog.value = true
await loadProviders()
}
async function loadProviders() {
loading.value = true
try {
const response = await axios.get('/api/config/provider/list', {
params: {
provider_type: props.providerType
}
})
if (response.data.status === 'ok') {
providerList.value = response.data.data || []
}
} catch (error) {
console.error('加载提供商列表失败:', error)
providerList.value = []
} finally {
loading.value = false
}
}
function selectProvider(provider) {
selectedProvider.value = provider.id
}
function confirmSelection() {
emit('update:modelValue', selectedProvider.value)
dialog.value = false
}
function cancelSelection() {
selectedProvider.value = props.modelValue || ''
dialog.value = false
}
</script>
<style scoped>
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
}
.v-list-item.v-list-item--active {
background-color: rgba(var(--v-theme-primary), 0.08);
}
</style>
@@ -30,7 +30,11 @@
"configApplyError": "配置未应用,Json 格式错误。",
"saveSuccess": "配置保存成功",
"saveError": "配置保存失败",
"loadError": "配置加载失败"
"loadError": "配置加载失败",
"deleteSuccess": "删除成功",
"deleteError": "删除失败",
"updateSuccess": "更新成功",
"updateError": "更新失败"
},
"sections": {
"general": "常规设置",
@@ -1,6 +1,5 @@
<script setup>
import { ref, shallowRef, onMounted } from 'vue';
import axios from 'axios';
import { ref, shallowRef } from 'vue';
import { useCustomizerStore } from '../../../stores/customizer';
import { useI18n } from '@/i18n/composables';
import sidebarItems from './sidebarItem';
+549 -142
View File
@@ -1,124 +1,175 @@
<template>
<v-card style="margin-bottom: 16px;">
<v-card-text style="padding: 0;">
<div>
<v-btn-group variant="outlined" divided>
<v-btn icon="mdi-text-box-edit-outline" style="width: 80px;" :color="editorTab === 0 ? 'primary' : ''"
@click="editorTab = 0">
<div style="display: flex; flex-direction: column; align-items: center;">
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
style="display: flex; flex-direction: column; align-items: start;">
<!-- 普通配置选择区域 -->
<div class="d-flex flex-row pr-4"
style="margin-bottom: 16px; align-items: center; gap: 12px; justify-content: space-between; width: 100%;">
<div class="d-flex flex-row align-center" style="gap: 12px;" >
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" v-if="!isSystemConfig"
item-value="id" label="选择配置文件" hide-details density="compact" rounded="md" variant="outlined"
@update:model-value="onConfigSelect">
<template v-slot:item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps"
:subtitle="item.raw.id === '_%manage%_' ? '管理所有配置文件' : formatUmop(item.raw.umop)"
:class="item.raw.id === '_%manage%_' ? 'text-primary' : ''">
</v-list-item>
</template>
</v-select>
</div>
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
rounded="md" @update:model-value="onConfigTypeToggle">
<v-btn value="normal" prepend-icon="mdi-cog" size="large" >
普通
</v-btn>
<v-btn icon="mdi-code-json" style="width: 80px;" :color="editorTab === 1 ? 'primary' : ''"
@click="configToString(); editorTab = 1;"></v-btn>
</v-btn-group>
<v-btn v-if="editorTab === 1" style="margin-left: 16px;" size="small" @click="configToString()">{{ tm('editor.revertCode') }}</v-btn>
<v-btn v-if="editorTab === 1 && config_data_has_changed" style="margin-left: 16px;" size="small"
@click="applyStrConfig()">{{ tm('editor.applyConfig') }}</v-btn>
<small v-if="editorTab === 1" style="margin-left: 16px;">💡 {{ tm('editor.applyTip') }}</small>
<v-btn value="system" prepend-icon="mdi-cog-outline" size="large">
系统
</v-btn>
</v-btn-toggle>
</div>
</v-card-text>
</v-card>
<v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear>
<!-- 可视化编辑 -->
<v-card v-if="editorTab === 0">
<v-tabs v-model="tab" align-tabs="left" color="deep-purple-accent-4">
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
style="font-weight: 1000; font-size: 15px">
{{ metadata[key]['name'] }}
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab">
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
<v-container fluid>
<div v-if="(selectedConfigID || isSystemConfig) && fetched" style="width: 100%;">
<!-- 可视化编辑 -->
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
style="font-weight: 1000; font-size: 15px">
{{ metadata[key]['name'] }}
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab" class="config-tabs-window">
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
<v-container fluid>
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
<!-- Support both traditional and JSON selector metadata -->
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
:metadataKey="key2">
</AstrBotConfigV4>
</div>
</v-container>
</v-tabs-window-item>
<div v-for="(val2, key2, index2) in metadata[key]['metadata']">
<!-- <h3>{{ metadata[key]['metadata'][key2]['description'] }}</h3> -->
<div v-if="metadata[key]['metadata'][key2]?.config_template"
v-show="key2 !== 'platform' && key2 !== 'provider'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
<!-- 带有 config_template 的配置项 -->
<v-list-item-title style="font-weight: bold;">
{{ metadata[key]['metadata'][key2]['description'] }} ({{ key2 }})
</v-list-item-title>
<v-tabs style="margin-top: 16px;" align-tabs="left" color="deep-purple-accent-4"
v-model="config_template_tab">
<v-tab v-if="metadata[key]['metadata'][key2]?.tmpl_display_title"
v-for="(item, index) in config_data[key2]" :key="index" :value="index">
{{ item[metadata[key]['metadata'][key2]?.tmpl_display_title] }}
</v-tab>
<v-tab v-else v-for="(item, index) in config_data[key2]" :key="index + '_'" :value="index">
{{ item.id }}({{ item.type }})
</v-tab>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="plain" size="large" v-bind="props">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-list @update:selected="addFromDefaultConfigTmpl($event, key, key2)">
<v-list-item v-for="(item, index) in metadata[key]['metadata'][key2]?.config_template" :key="index"
:value="index">
<v-list-item-title>{{ index }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-tabs>
<v-tabs-window v-model="config_template_tab">
<v-tabs-window-item v-for="(config_item, index) in config_data[key2]"
v-show="config_template_tab === index" :key="index" :value="index">
<div style="padding: 16px;">
<v-btn variant="tonal" rounded="xl" color="error" @click="deleteItem(key2, index)">
{{ tm('actions.delete') }}
</v-btn>
<AstrBotConfig :metadata="metadata[key]['metadata']" :iterable="config_item" :metadataKey="key2">
</AstrBotConfig>
</div>
</v-tabs-window-item>
</v-tabs-window>
<div style="margin-left: 16px; padding-bottom: 16px">
<small>{{ tm('help.helpPrefix') }}
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
{{ tm('help.helpMiddle') }}
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
</small>
</div>
<div v-else>
<!-- 如果配置项是一个 object那么 iterable 需要取到这个 object 的值否则取到整个 config_data -->
<div v-if="metadata[key]['metadata'][key2]['type'] == 'object'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
<AstrBotConfig
:metadata="metadata[key]['metadata']" :iterable="config_data[key2]" :metadataKey="key2">
</AstrBotConfig>
</v-tabs-window>
</div>
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
color="darkprimary" @click="updateConfig">
</v-btn>
<v-btn icon="mdi-code-json" size="x-large" style="position: fixed; right: 52px; bottom: 124px;" color="primary"
@click="configToString(); codeEditorDialog = true">
</v-btn>
</div>
</div>
</div>
<!-- Full Screen Editor Dialog -->
<v-dialog v-model="codeEditorDialog" fullscreen transition="dialog-bottom-transition" scrollable>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="codeEditorDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>编辑配置文件</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items style="display: flex; align-items: center;">
<v-btn style="margin-left: 16px;" size="small" @click="configToString()">{{
tm('editor.revertCode') }}</v-btn>
<v-btn v-if="config_data_has_changed" style="margin-left: 16px;" size="small" @click="applyStrConfig()">{{
tm('editor.applyConfig') }}</v-btn>
<small style="margin-left: 16px;">💡 {{ tm('editor.applyTip') }}</small>
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
<VueMonacoEditor language="json" theme="vs-dark" style="height: calc(100vh - 64px);"
v-model:value="config_data_str">
</VueMonacoEditor>
</v-card-text>
</v-card>
</v-dialog>
<!-- Config Management Dialog -->
<v-dialog v-model="configManageDialog" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span class="text-h4">配置文件管理</span>
<v-btn icon="mdi-close" variant="text" @click="configManageDialog = false"></v-btn>
</v-card-title>
<v-card-text>
<small>AstrBot 支持针对不同消息平台实例分别设置配置文件默认会使用 `default` 配置</small>
<div class="mt-6 mb-4">
<v-btn prepend-icon="mdi-plus" @click="startCreateConfig" variant="tonal" color="primary">
新建配置文件
</v-btn>
</div>
<!-- Config List -->
<v-list lines="two">
<v-list-item v-for="config in configInfoList" :key="config.id" :title="config.name">
<v-list-item-subtitle>当前应用于: {{ formatUmop(config.umop) }} </v-list-item-subtitle>
<template v-slot:append v-if="config.id !== 'default'">
<div class="d-flex align-center" style="gap: 8px;">
<v-btn icon="mdi-pencil" size="small" variant="text" color="warning"
@click="startEditConfig(config)"></v-btn>
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
@click="confirmDeleteConfig(config)"></v-btn>
</div>
<AstrBotConfig v-else :metadata="metadata[key]['metadata']" :iterable="config_data" :metadataKey="key2">
</AstrBotConfig>
</div>
</template>
</v-list-item>
</v-list>
<!-- Create/Edit Form -->
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
<div v-if="showConfigForm">
<h3 class="mb-4">{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}</h3>
<div class="mb-4">
<small v-if="conflictMessage"> {{ conflictMessage }}</small>
</div>
<v-text-field v-model="configFormData.name" label="配置文件名称" variant="outlined" class="mb-4"
hide-details></v-text-field>
<v-select v-model="configFormData.umop" :items="platformList" item-title="id" item-value="id" label="应用于平台"
variant="outlined" hide-details multiple @update:model-value="checkPlatformConflictOnForm">
<template v-slot:item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps" :subtitle="item.raw.type"></v-list-item>
</template>
</v-select>
</v-container>
</v-tabs-window-item>
<div style="margin-left: 16px; padding-bottom: 16px">
<small>{{ tm('help.helpPrefix') }}
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
{{ tm('help.helpMiddle') }}
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft" target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
</small>
</div>
</v-tabs-window>
</v-card>
<!-- 代码编辑 -->
<v-card v-else style="background-color: #1e1e1e;">
<VueMonacoEditor theme="vs-dark" language="json" height="80vh" style="padding-top: 16px; padding-bottom: 16px;"
v-model:value="config_data_str">
</VueMonacoEditor>
</v-card>
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;" color="darkprimary"
@click="updateConfig">
</v-btn>
<div class="d-flex justify-end mt-4" style="gap: 8px;">
<v-btn variant="text" @click="cancelConfigForm">取消</v-btn>
<v-btn color="primary" @click="saveConfigForm"
:disabled="!configFormData.name || !configFormData.umop.length || !!conflictMessage">
{{ isEditingConfig ? '更新' : '创建' }}
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack">
{{ save_message }}
@@ -130,23 +181,22 @@
<script>
import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import config from '@/config';
import { useI18n, useModuleI18n } from '@/i18n/composables';
export default {
name: 'ConfigPage',
components: {
AstrBotConfig,
AstrBotConfigV4,
VueMonacoEditor,
WaitingForRestart
},
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/config');
return {
t,
tm
@@ -154,7 +204,6 @@ export default {
},
computed: {
// 安全访问翻译的计算属性
messages() {
return {
loadError: this.tm('messages.loadError'),
@@ -163,7 +212,22 @@ export default {
configApplied: this.tm('messages.configApplied'),
configApplyError: this.tm('messages.configApplyError')
};
}
},
configInfoNameList() {
return this.configInfoList.map(info => info.name);
},
selectedConfigInfo() {
return this.configInfoList.find(info => info.id === this.selectedConfigID) || {};
},
configSelectItems() {
const items = [...this.configInfoList];
items.push({
id: '_%manage%_',
name: '管理配置文件...',
umop: []
});
return items;
},
},
watch: {
config_data_str: function (val) {
@@ -172,6 +236,10 @@ export default {
},
data() {
return {
codeEditorDialog: false,
configManageDialog: false,
showConfigForm: false,
isEditingConfig: false,
config_data_has_changed: false,
config_data_str: "",
config_data: {
@@ -179,30 +247,79 @@ export default {
},
fetched: false,
metadata: {},
provider_config_tmpl: {},
adapter_config_tmpl: {}, // 平台适配器
save_message_snack: false,
save_message: "",
save_message_success: "",
namespace: "",
tab: 0,
editorTab: 0, // 0: visual, 1: code
config_template_tab: 0,
tab: 0, // 用于切换配置标签页
// 配置类型切换
configType: 'normal', // 'normal' 或 'system'
// 系统配置开关
isSystemConfig: false,
// 多配置文件管理
selectedConfigID: null, // 用于存储当前选中的配置项信息
configInfoList: [],
platformList: [],
configFormData: {
name: '',
umop: [],
},
editingConfigId: null,
conflictMessage: '', // 冲突提示信息
}
},
mounted() {
this.getConfig();
this.getConfigInfoList("default");
// 初始化配置类型状态
this.configType = this.isSystemConfig ? 'system' : 'normal';
},
methods: {
getConfig() {
// 获取配置
axios.get('/api/config/get').then((res) => {
getConfigInfoList(abconf_id) {
// 获取配置列表
axios.get('/api/config/abconfs').then((res) => {
this.configInfoList = res.data.data.info_list;
if (abconf_id) {
for (let i = 0; i < this.configInfoList.length; i++) {
if (this.configInfoList[i].id === abconf_id) {
this.selectedConfigID = this.configInfoList[i].id
this.getConfig(abconf_id);
break;
}
}
}
}).catch((err) => {
this.save_message = this.messages.loadError;
this.save_message_snack = true;
this.save_message_success = "error";
});
},
getPlatformList() {
axios.get('/api/config/platform/list').then((res) => {
this.platformList = res.data.data.platforms;
}).catch((err) => {
console.error(this.t('status.dataError'), err);
});
},
getConfig(abconf_id) {
this.fetched = false
const params = {};
if (this.isSystemConfig) {
params.system_config = '1';
} else {
params.id = abconf_id || this.selectedConfigID;
}
axios.get('/api/config/abconf', {
params: params
}).then((res) => {
this.config_data = res.data.data.config;
this.fetched = true
this.metadata = res.data.data.metadata;
this.provider_config_tmpl = res.data.data.provider_config_tmpl;
this.adapter_config_tmpl = res.data.data.adapter_config_tmpl;
}).catch((err) => {
this.save_message = this.messages.loadError;
this.save_message_snack = true;
@@ -211,12 +328,28 @@ export default {
},
updateConfig() {
if (!this.fetched) return;
axios.post('/api/config/astrbot/update', this.config_data).then((res) => {
const postData = {
config: this.config_data
};
if (this.isSystemConfig) {
postData.conf_id = 'default';
} else {
postData.conf_id = this.selectedConfigID;
}
axios.post('/api/config/astrbot/update', postData).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message || this.messages.saveSuccess;
this.save_message_snack = true;
this.save_message_success = "success";
this.$refs.wfr.check();
if (this.isSystemConfig) {
axios.post('/api/stat/restart-core').then(() => {
this.$refs.wfr.check();
})
}
} else {
this.save_message = res.data.message || this.messages.saveError;
this.save_message_snack = true;
@@ -245,27 +378,248 @@ export default {
this.save_message_snack = true;
}
},
addFromDefaultConfigTmpl(val, group_name, config_item_name) {
console.log(val);
createNewConfig() {
// 修正为 umo part 形式
// 暂时只支持 platform:: 形式
const umo_parts = this.configFormData.umop.map(platform => platform + "::");
let tmpl = this.metadata[group_name]['metadata'][config_item_name]['config_template'][val];
let new_tmpl_cfg = JSON.parse(JSON.stringify(tmpl));
// new_tmpl_cfg.id = "new_" + val + "_" + this.config_data[config_item_name].length;
this.config_data[config_item_name].push(new_tmpl_cfg);
this.config_template_tab = this.config_data[config_item_name].length - 1;
axios.post('/api/config/abconf/new', {
umo_parts: umo_parts,
name: this.configFormData.name
}).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "success";
this.getConfigInfoList(res.data.data.conf_id);
this.cancelConfigForm();
} else {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "error";
}
}).catch((err) => {
console.error(err);
this.save_message = "新配置文件创建失败";
this.save_message_snack = true;
this.save_message_success = "error";
});
},
deleteItem(config_item_name, index) {
console.log(config_item_name, index);
let new_list = [];
for (let i = 0; i < this.config_data[config_item_name].length; i++) {
if (i !== index) {
new_list.push(this.config_data[config_item_name][i]);
checkPlatformConflict(newPlatforms) {
const conflictConfigs = [];
// 遍历现有的配置文件,排除名为 "default" 的配置
for (const config of this.configInfoList) {
if (config.name === 'default') {
continue; // 跳过 default 配置
}
if (config.umop && config.umop.length > 0) {
// 获取现有配置的平台列表
const existingPlatforms = config.umop.map(umop => {
const platformPart = umop.split(":")[0];
return platformPart === "" ? "*" : platformPart; // 空字符串表示所有平台
});
// 检查是否有重复的平台
const hasConflict = newPlatforms.some(newPlatform => {
return existingPlatforms.includes(newPlatform) || existingPlatforms.includes("*");
}) || (newPlatforms.includes("*") && existingPlatforms.length > 0);
if (hasConflict) {
conflictConfigs.push(config);
}
}
}
this.config_data[config_item_name] = new_list;
if (this.config_template_tab > 0) {
this.config_template_tab -= 1;
return conflictConfigs;
},
onConfigSelect(value) {
if (value === '_%manage%_') {
this.configManageDialog = true;
this.getPlatformList();
// 重置选择到之前的值
this.$nextTick(() => {
this.selectedConfigID = this.selectedConfigInfo.id || 'default';
});
} else {
this.getConfig(value);
}
},
startCreateConfig() {
this.showConfigForm = true;
this.isEditingConfig = false;
this.configFormData = {
name: '',
umop: [],
};
this.editingConfigId = null;
this.conflictMessage = '';
},
startEditConfig(config) {
this.showConfigForm = true;
this.isEditingConfig = true;
this.editingConfigId = config.id;
this.configFormData = {
name: config.name || '',
umop: config.umop ? config.umop.map(part => part.split("::")[0]).filter(p => p) : [],
};
this.conflictMessage = '';
},
cancelConfigForm() {
this.showConfigForm = false;
this.isEditingConfig = false;
this.editingConfigId = null;
this.configFormData = {
name: '',
umop: [],
};
this.conflictMessage = '';
},
saveConfigForm() {
if (!this.configFormData.name || !this.configFormData.umop.length) {
this.save_message = "请填写配置名称和选择应用平台";
this.save_message_snack = true;
this.save_message_success = "error";
return;
}
if (this.conflictMessage) {
return;
}
if (this.isEditingConfig) {
this.updateConfigInfo();
} else {
this.createNewConfig();
}
},
confirmDeleteConfig(config) {
if (confirm(`确定要删除配置文件 "${config.name}" 吗?此操作不可恢复。`)) {
this.deleteConfig(config.id);
}
},
deleteConfig(configId) {
axios.post('/api/config/abconf/delete', {
id: configId
}).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "success";
// 删除成功后,更新配置列表
this.getConfigInfoList("default");
} else {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "error";
}
}).catch((err) => {
console.error(err);
this.save_message = "删除配置文件失败";
this.save_message_snack = true;
this.save_message_success = "error";
});
},
checkPlatformConflictOnForm() {
if (!this.configFormData.umop || this.configFormData.umop.length === 0) {
this.conflictMessage = '';
return;
}
// 检查与其他配置文件的冲突
let conflictConfigs = this.checkPlatformConflict(this.configFormData.umop);
// 如果是编辑模式,排除当前编辑的配置文件
if (this.isEditingConfig && this.editingConfigId) {
conflictConfigs = conflictConfigs.filter(config => config.id !== this.editingConfigId);
}
if (conflictConfigs.length > 0) {
const conflictNames = conflictConfigs.map(config => config.name).join(', ');
this.conflictMessage = `提示:选择的平台与现有配置文件重复:${conflictNames}。AstrBot 将只会应用首个匹配的配置文件。`;
} else {
this.conflictMessage = '';
}
},
updateConfigInfo() {
// 修正为 umo part 形式
// 暂时只支持 platform:: 形式
const umo_parts = this.configFormData.umop.map(platform => platform + "::");
axios.post('/api/config/abconf/update', {
id: this.editingConfigId,
name: this.configFormData.name,
umo_parts: umo_parts
}).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "success";
this.getConfigInfoList(this.editingConfigId);
this.cancelConfigForm();
} else {
this.save_message = res.data.message;
this.save_message_snack = true;
this.save_message_success = "error";
}
}).catch((err) => {
console.error(err);
this.save_message = "更新配置文件失败";
this.save_message_snack = true;
this.save_message_success = "error";
});
},
formatUmop(umop) {
if (!umop) {
return
}
let ret = ""
for (let i = 0; i < umop.length; i++) {
let platformPart = umop[i].split(":")[0];
if (platformPart === "") {
return "所有平台";
} else {
ret += platformPart + ",";
}
}
ret = ret.slice(0, -1);
return ret;
},
onConfigTypeToggle() {
this.isSystemConfig = this.configType === 'system';
this.tab = 0; // 重置标签页
this.fetched = false; // 重置加载状态
if (this.isSystemConfig) {
// 切换到系统配置
this.getConfig();
} else {
// 切换回普通配置,如果有选中的配置文件则加载,否则加载default
if (this.selectedConfigID) {
this.getConfig(this.selectedConfigID);
} else {
this.getConfigInfoList("default");
}
}
},
onSystemConfigToggle() {
// 保持向后兼容性,更新 configType
this.configType = this.isSystemConfig ? 'system' : 'normal';
this.tab = 0; // 重置标签页
this.fetched = false; // 重置加载状态
if (this.isSystemConfig) {
// 切换到系统配置
this.getConfig();
} else {
// 切换回普通配置,如果有选中的配置文件则加载,否则加载default
if (this.selectedConfigID) {
this.getConfig(this.selectedConfigID);
} else {
this.getConfigInfoList("default");
}
}
}
},
@@ -277,4 +631,57 @@ export default {
.v-tab {
text-transform: none !important;
}
/* 按钮切换样式优化 */
.v-btn-toggle .v-btn {
transition: all 0.3s ease !important;
}
.v-btn-toggle .v-btn:not(.v-btn--active) {
opacity: 0.7;
}
.v-btn-toggle .v-btn.v-btn--active {
opacity: 1;
font-weight: 600;
}
@media (min-width: 768px) {
.config-tabs {
display: flex;
margin: 16px 16px 0 0;
}
.config-panel {
width: 750px;
}
.config-tabs-window {
flex: 1;
}
.config-tabs .v-tab {
justify-content: flex-start !important;
text-align: left;
min-height: 48px;
}
}
@media (max-width: 767px) {
.config-tabs {
width: 100%;
}
.v-container {
padding: 4px;
}
.config-panel {
width: 100%;
}
.config-tabs-window {
margin-top: 16px;
}
}
</style>
-5
View File
@@ -151,8 +151,6 @@
{{ save_message }}
</v-snackbar>
<WaitingForRestart ref="wfr"></WaitingForRestart>
<!-- ID冲突确认对话框 -->
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
<v-card>
@@ -359,7 +357,6 @@ export default {
this.loading = false;
this.showPlatformCfg = false;
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || this.messages.updateSuccess);
}).catch((err) => {
this.loading = false;
@@ -413,7 +410,6 @@ export default {
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || this.messages.deleteSuccess);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
@@ -429,7 +425,6 @@ export default {
config: platform
}).then((res) => {
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || this.messages.statusUpdateSuccess);
}).catch((err) => {
platform.enable = !platform.enable; // 发生错误时回滚状态
-80
View File
@@ -12,9 +12,6 @@
</p>
</div>
<div>
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true" rounded="xl" size="x-large">
{{ tm('providers.settings') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true" rounded="xl" size="x-large">
{{ tm('providers.addProvider') }}
</v-btn>
@@ -250,49 +247,6 @@
</v-card>
</v-dialog>
<!-- 设置对话框 -->
<v-dialog v-model="showSettingsDialog" max-width="600px">
<v-card>
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-cog</v-icon>
<span>{{ tm('dialogs.settings.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showSettingsDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4">
<v-list>
<v-list-item>
<v-switch
style="padding: 12px;"
v-model="sessionSeparationEnabled"
color="primary"
:loading="sessionSettingLoading"
@change="updateSessionSeparation"
hide-details
>
<template v-slot:label>
<div>
<div class="text-subtitle-1">{{ tm('dialogs.settings.sessionSeparation.title') }}</div>
<div class="text-caption text-medium-emphasis">{{ tm('dialogs.settings.sessionSeparation.description') }}</div>
</div>
</template>
</v-switch>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showSettingsDialog = false">
{{ tm('dialogs.settings.close') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 消息提示 -->
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
location="top">
@@ -365,11 +319,6 @@ export default {
metadata: {},
showProviderCfg: false,
// 设置对话框相关
showSettingsDialog: false,
sessionSeparationEnabled: false,
sessionSettingLoading: false,
// ID冲突确认对话框
showIdConflictDialog: false,
conflictId: '',
@@ -464,10 +413,8 @@ export default {
add: this.tm('messages.success.add'),
delete: this.tm('messages.success.delete'),
statusUpdate: this.tm('messages.success.statusUpdate'),
sessionSeparation: this.tm('messages.success.sessionSeparation')
},
error: {
sessionSeparation: this.tm('messages.error.sessionSeparation'),
fetchStatus: this.tm('messages.error.fetchStatus')
},
confirm: {
@@ -502,7 +449,6 @@ export default {
mounted() {
this.getConfig();
this.getSessionSeparationStatus();
},
methods: {
@@ -722,32 +668,6 @@ export default {
});
},
// 获取会话隔离配置状态
getSessionSeparationStatus() {
axios.get('/api/config/provider/get_session_seperate').then((res) => {
if (res.data && res.data.status === 'ok') {
this.sessionSeparationEnabled = res.data.data.enable;
}
}).catch((err) => {
this.showError(err.response?.data?.message || this.messages.error.sessionSeparation);
});
},
// 更新会话隔离配置
updateSessionSeparation() {
this.sessionSettingLoading = true;
axios.post('/api/config/provider/set_session_seperate', {
enable: this.sessionSeparationEnabled
}).then((res) => {
this.showSuccess(res.data.message || this.messages.success.sessionSeparation);
this.sessionSettingLoading = false;
}).catch((err) => {
this.sessionSeparationEnabled = !this.sessionSeparationEnabled; // 发生错误时回滚状态
this.showError(err.response?.data?.message || err.message);
this.sessionSettingLoading = false;
});
},
showSuccess(message) {
this.save_message = message;
this.save_message_success = "success";