feat: allow copy config from existing configs (#6785)

* feat: allow copy config from existing configs

* fix: issues mentioned by reviewer bot

- duplicated logic for initializing/resetting the config
- check whitespace-only names
This commit is contained in:
Flartiny
2026-03-28 21:25:19 +08:00
committed by GitHub
parent bbec8efa0d
commit da1eb65afe
4 changed files with 126 additions and 33 deletions
@@ -77,13 +77,16 @@
"title": "Configuration Management",
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
"newConfig": "New Configuration",
"copyConfig": "Copy Configuration",
"editConfig": "Edit Configuration",
"manageConfigs": "Manage Configurations...",
"configName": "Name",
"fillConfigName": "Enter configuration name",
"confirmDelete": "Are you sure you want to delete the configuration \"{name}\"? This action cannot be undone.",
"pleaseEnterName": "Please enter a configuration name",
"nameExists": "Configuration name already exists. Please use another name.",
"createFailed": "Failed to create new configuration",
"copyFailed": "Failed to copy configuration",
"deleteFailed": "Failed to delete configuration",
"updateFailed": "Failed to update configuration"
},
@@ -77,13 +77,16 @@
"title": "Управление конфигурациями",
"description": "AstrBot поддерживает несколько конфигураций для разных ботов. По умолчанию используется «default».",
"newConfig": "Новая конфигурация",
"copyConfig": "Копировать конфигурацию",
"editConfig": "Изменить конфигурацию",
"manageConfigs": "Управление файлами...",
"configName": "Имя",
"fillConfigName": "Введите имя конфигурации",
"confirmDelete": "Вы уверены, что хотите удалить конфигурацию «{name}»? Это действие необратимо.",
"pleaseEnterName": "Пожалуйста, введите имя",
"nameExists": "Имя конфигурации уже существует. Используйте другое имя.",
"createFailed": "Ошибка создания конфигурации",
"copyFailed": "Ошибка копирования конфигурации",
"deleteFailed": "Ошибка удаления",
"updateFailed": "Ошибка обновления"
},
@@ -126,4 +129,4 @@
"cancel": "Отмена"
}
}
}
}
@@ -77,13 +77,16 @@
"title": "配置文件管理",
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
"newConfig": "新建配置文件",
"copyConfig": "复制配置文件",
"editConfig": "编辑配置文件",
"manageConfigs": "管理配置文件...",
"configName": "名称",
"fillConfigName": "填写配置文件名称",
"confirmDelete": "确定要删除配置文件 \"{name}\" 吗?此操作不可恢复。",
"pleaseEnterName": "请填写配置名称",
"nameExists": "配置文件名称已存在,请使用其他名称",
"createFailed": "新配置文件创建失败",
"copyFailed": "复制配置文件失败",
"deleteFailed": "删除配置文件失败",
"updateFailed": "更新配置文件失败"
},
+116 -32
View File
@@ -126,11 +126,15 @@
<!-- Config List -->
<v-list lines="two">
<v-list-item v-for="config in configInfoList" :key="config.id" :title="config.name">
<template v-slot:append v-if="config.id !== 'default'">
<template v-slot:append>
<div class="d-flex align-center" style="gap: 8px;">
<v-btn icon="mdi-content-copy" size="small" variant="text" color="primary"
@click="startCopyConfig(config)"></v-btn>
<v-btn icon="mdi-pencil" size="small" variant="text" color="warning"
v-if="config.id !== 'default'"
@click="startEditConfig(config)"></v-btn>
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
v-if="config.id !== 'default'"
@click="confirmDeleteConfig(config)"></v-btn>
</div>
</template>
@@ -141,7 +145,7 @@
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
<div v-if="showConfigForm">
<h3 class="mb-4">{{ isEditingConfig ? tm('configManagement.editConfig') : tm('configManagement.newConfig') }}</h3>
<h3 class="mb-4">{{ configFormTitle }}</h3>
<h4>{{ tm('configManagement.configName') }}</h4>
@@ -151,7 +155,7 @@
<div class="d-flex justify-end mt-4" style="gap: 8px;">
<v-btn variant="text" @click="cancelConfigForm">{{ tm('buttons.cancel') }}</v-btn>
<v-btn color="primary" @click="saveConfigForm"
:disabled="!configFormData.name">
:disabled="isConfigFormSaveDisabled">
{{ isEditingConfig ? tm('buttons.update') : tm('buttons.create') }}
</v-btn>
</div>
@@ -297,6 +301,19 @@ export default {
selectedConfigInfo() {
return this.configInfoList.find(info => info.id === this.selectedConfigID) || {};
},
configFormTitle() {
if (this.isEditingConfig) {
return this.tm('configManagement.editConfig');
}
if (this.isCopyingConfig) {
return this.tm('configManagement.copyConfig');
}
return this.tm('configManagement.newConfig');
},
isConfigFormSaveDisabled() {
const isNameEmpty = !this.normalizeConfigName(this.configFormData.name);
return isNameEmpty || (this.isCopyingConfig && !this.copySourceConfigId);
},
configSelectItems() {
const items = [...this.configInfoList];
items.push({
@@ -343,6 +360,7 @@ export default {
configManageDialog: false,
showConfigForm: false,
isEditingConfig: false,
isCopyingConfig: false,
config_data_has_changed: false,
config_data_str: "",
config_data: {
@@ -371,6 +389,7 @@ export default {
name: '',
},
editingConfigId: null,
copySourceConfigId: '',
// 测试聊天
testChatDrawer: false,
@@ -567,9 +586,9 @@ export default {
this.save_message_snack = true;
}
},
createNewConfig() {
createNewConfig(configName) {
axios.post('/api/config/abconf/new', {
name: this.configFormData.name
name: configName
}).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;
@@ -589,6 +608,24 @@ export default {
this.save_message_success = "error";
});
},
normalizeConfigName(name) {
return typeof name === 'string' ? name.trim() : '';
},
hasDuplicateConfigName(name, excludeId = null) {
const normalizedName = this.normalizeConfigName(name);
if (!normalizedName) {
return false;
}
return this.configInfoList.some((config) => {
if (!config || !config.name) {
return false;
}
if (excludeId && config.id === excludeId) {
return false;
}
return this.normalizeConfigName(config.name) === normalizedName;
});
},
async onConfigSelect(value) {
if (value === '_%manage%_') {
this.configManageDialog = true;
@@ -638,44 +675,91 @@ export default {
}
}
},
setConfigFormState({ mode = 'create', config = null, visible = true } = {}) {
this.showConfigForm = visible;
this.isEditingConfig = mode === 'edit';
this.isCopyingConfig = mode === 'copy';
this.editingConfigId = this.isEditingConfig && config ? config.id : null;
this.copySourceConfigId = this.isCopyingConfig && config ? config.id : '';
let name = '';
if (this.isEditingConfig && config) {
name = config.name || '';
} else if (this.isCopyingConfig && config) {
name = `${config.name || ''}-copy`;
}
this.configFormData = { name };
},
startCreateConfig() {
this.showConfigForm = true;
this.isEditingConfig = false;
this.configFormData = {
name: '',
};
this.editingConfigId = null;
this.setConfigFormState({ mode: 'create' });
},
startEditConfig(config) {
this.showConfigForm = true;
this.isEditingConfig = true;
this.editingConfigId = config.id;
this.configFormData = {
name: config.name || '',
};
this.setConfigFormState({ mode: 'edit', config });
},
startCopyConfig(config) {
this.setConfigFormState({ mode: 'copy', config });
},
cancelConfigForm() {
this.showConfigForm = false;
this.isEditingConfig = false;
this.editingConfigId = null;
this.configFormData = {
name: '',
};
this.setConfigFormState({ visible: false });
},
saveConfigForm() {
if (!this.configFormData.name) {
const normalizedName = this.normalizeConfigName(this.configFormData.name);
if (!normalizedName) {
this.save_message = this.tm('configManagement.pleaseEnterName');
this.save_message_snack = true;
this.save_message_success = "error";
return;
}
if (this.isEditingConfig) {
this.updateConfigInfo();
} else {
this.createNewConfig();
const excludeId = this.isEditingConfig ? this.editingConfigId : null;
if (this.hasDuplicateConfigName(normalizedName, excludeId)) {
this.save_message = this.tm('configManagement.nameExists');
this.save_message_snack = true;
this.save_message_success = "error";
return;
}
this.configFormData.name = normalizedName;
if (this.isEditingConfig) {
this.updateConfigInfo(normalizedName);
} else if (this.isCopyingConfig) {
this.copyConfig(normalizedName);
} else {
this.createNewConfig(normalizedName);
}
},
copyConfig(configName) {
axios.get('/api/config/abconf', {
params: { id: this.copySourceConfigId }
}).then((res) => {
const sourceConfig = res.data?.data?.config;
if (!sourceConfig) {
this.save_message = this.tm('configManagement.copyFailed');
this.save_message_snack = true;
this.save_message_success = "error";
return;
}
return axios.post('/api/config/abconf/new', {
name: configName,
config: sourceConfig
});
}).then((res) => {
if (!res) return;
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 = err?.response?.data?.message || this.tm('configManagement.copyFailed');
this.save_message_snack = true;
this.save_message_success = "error";
});
},
async confirmDeleteConfig(config) {
const message = this.tm('configManagement.confirmDelete').replace('{name}', config.name);
@@ -706,10 +790,10 @@ export default {
this.save_message_success = "error";
});
},
updateConfigInfo() {
updateConfigInfo(configName) {
axios.post('/api/config/abconf/update', {
id: this.editingConfigId,
name: this.configFormData.name
name: configName
}).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;