astrbbbb / dashboard /src /components /platform /AddNewPlatform.vue
qa1145's picture
Upload 1245 files
8ede856 verified
<template>
<v-dialog v-model="showDialog" max-width="800px" max-height="90%" @after-enter="prepareData">
<v-card
:title="updatingMode ? `${tm('dialog.edit')} ${updatingPlatformConfig.id} ${tm('dialog.adapter')}` : tm('dialog.addPlatform')">
<v-card-text ref="dialogScrollContainer" class="pa-4 ml-2" style="overflow-y: auto;">
<div class="d-flex align-start" style="width: 100%;">
<div>
<v-icon icon="mdi-numeric-1-circle" class="mr-3"></v-icon>
</div>
<div style="flex: 1;">
<h3>
{{ tm('createDialog.step1Title') }}
</h3>
<small style="color: grey;">{{ tm('createDialog.step1Hint') }}</small>
<div>
<div v-if="!updatingMode">
<v-select v-model="selectedPlatformType" :items="Object.keys(platformTemplates)" item-title="name"
item-value="name" :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
style="max-width: 30%; min-width: 300px;">
<template v-slot:item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template v-slot:prepend>
<img :src="getPlatformIcon(platformTemplates[item.raw].type)"
style="width: 32px; height: 32px; object-fit: contain; margin-right: 16px;" />
</template>
</v-list-item>
</template>
</v-select>
<div class="mt-3" v-if="selectedPlatformConfig">
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
<v-icon start>mdi-book-open-variant</v-icon>
{{ tm('dialog.viewTutorial') }}
</v-btn>
<div class="mt-2">
<AstrBotConfig :iterable="selectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
metadataKey="platform" />
</div>
</div>
</div>
<div v-else>
<v-text-field :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
style="max-width: 30%; min-width: 300px;" v-model="updatingPlatformConfig.type"
disabled></v-text-field>
<div class="mt-3">
<div class="mt-2">
<AstrBotConfig :iterable="updatingPlatformConfig" :metadata="metadata['platform_group']?.metadata"
metadataKey="platform" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-start mt-6">
<div>
<v-icon icon="mdi-numeric-2-circle" class="mr-3"></v-icon>
</div>
<div style="flex: 1;">
<div class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center">
<h3>
{{ tm('createDialog.configFileTitle') }}
</h3>
<v-chip size="x-small" color="primary" variant="tonal" rounded="sm" class="ml-2"
v-if="!updatingMode">{{ tm('createDialog.optional') }}</v-chip>
</div>
<small style="color: grey;">{{ tm('createDialog.configHint') }}</small>
<small style="color: grey;" v-if="!updatingMode">{{ tm('createDialog.configDefaultHint') }}</small>
</div>
<div>
<v-btn variant="plain" icon @click="toggleConfigSection" class="mt-2">
<v-icon>{{ showConfigSection ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</div>
</div>
<div v-if="showConfigSection">
<div v-if="!updatingMode">
<v-radio-group class="mt-2" v-model="aBConfigRadioVal" hide-details="true">
<v-radio value="0">
<template v-slot:label>
<span>{{ tm('createDialog.useExistingConfig') }}</span>
</template>
</v-radio>
<div class="d-flex align-center ml-10 my-2" v-if="aBConfigRadioVal === '0'">
<v-select v-model="selectedAbConfId" :items="configInfoList" item-title="name"
item-value="id" :label="tm('createDialog.selectConfigLabel')" variant="outlined" rounded="md" dense hide-details
style="max-width: 30%; min-width: 200px;">
</v-select>
<v-btn icon variant="text" density="comfortable" class="ml-2"
:disabled="!selectedAbConfId" @click="openConfigDrawer(selectedAbConfId)">
<v-icon>mdi-arrow-top-right-thick</v-icon>
</v-btn>
</div>
<v-radio value="1" :label="tm('createDialog.createNewConfig')">
</v-radio>
<div class="d-flex align-center" v-if="aBConfigRadioVal === '1'">
<v-text-field v-model="selectedAbConfId" :label="tm('createDialog.newConfigNameLabel')" variant="outlined" rounded="md" dense
hide-details style="max-width: 30%; min-width: 200px;" class="ml-10 my-2">
</v-text-field>
</div>
</v-radio-group>
<!-- 现有配置文件预览区域 -->
<!-- <div v-if="aBConfigRadioVal === '0' && selectedAbConfId" class="mt-4">
<div v-if="configPreviewLoading" class="d-flex justify-center py-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<div v-else-if="selectedConfigData && selectedConfigMetadata" class="config-preview-container">
<h4 class="mb-3">配置文件预览</h4>
<AstrBotCoreConfigWrapper :metadata="selectedConfigMetadata" :config_data="selectedConfigData"
readonly="true" />
</div>
<div v-else class="text-center py-4 text-grey">
<v-icon>mdi-information-outline</v-icon>
<p class="mt-2">无法加载配置文件预览</p>
</div>
</div> -->
<!-- 新配置文件编辑区域 -->
<div v-if="aBConfigRadioVal === '1'" class="mt-4">
<div v-if="newConfigLoading" class="d-flex justify-center py-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<div v-else-if="newConfigData && newConfigMetadata" class="config-preview-container">
<h4 class="mb-3">{{ tm('createDialog.newConfigTitle') }}</h4>
<AstrBotCoreConfigWrapper :metadata="newConfigMetadata" :config_data="newConfigData" />
</div>
<div v-else class="text-center py-4 text-grey">
<v-icon>mdi-information-outline</v-icon>
<p class="mt-2">{{ tm('createDialog.newConfigLoadFailed') }}</p>
</div>
</div>
</div>
<div v-else>
<div class="mb-3 d-flex align-center justify-space-between">
<div>
<v-btn v-if="isEditingRoutes" color="primary" variant="tonal" @click="addNewRoute" size="small">
<v-icon start>mdi-plus</v-icon>
{{ tm('createDialog.addRouteRule') }}
</v-btn>
</div>
<v-btn :color="isEditingRoutes ? 'grey' : 'primary'" variant="tonal" size="small"
@click="toggleEditMode">
<v-icon start>{{ isEditingRoutes ? 'mdi-eye' : 'mdi-pencil' }}</v-icon>
{{ isEditingRoutes ? tm('createDialog.viewMode') : tm('createDialog.editMode') }}
</v-btn>
</div>
<v-data-table :headers="routeTableHeaders" :items="platformRoutes" item-value="umop"
:no-data-text="tm('createDialog.noRouteRules')" hide-default-footer :items-per-page="-1" class="mt-2"
variant="outlined">
<template v-slot:item.source="{ item }">
<div class="d-flex align-center" style="min-width: 250px;">
<v-select v-if="isEditingRoutes" v-model="item.messageType" :items="messageTypeOptions"
item-title="label" item-value="value" variant="outlined" density="compact" hide-details
style="max-width: 140px;">
</v-select>
<small v-else>{{ getMessageTypeLabel(item.messageType) }}</small>
<small class="mx-1">:</small>
<v-text-field v-if="isEditingRoutes" v-model="item.sessionId" variant="outlined" density="compact"
hide-details :placeholder="tm('createDialog.sessionIdPlaceholder')">
</v-text-field>
<small v-else>{{ item.sessionId === '*' ? tm('createDialog.allSessions') : item.sessionId }}</small>
</div>
</template>
<template v-slot:item.configId="{ item }">
<div class="d-flex align-center">
<v-select v-if="isEditingRoutes" v-model="item.configId" :items="configInfoList"
item-title="name" item-value="id" variant="outlined" density="compact"
style="min-width: 200px;" hide-details>
</v-select>
<div v-else>
<small>{{ getConfigName(item.configId) }}</small>
</div>
<v-btn icon variant="text" density="compact" class="ml-2"
:disabled="!item.configId" @click="openConfigDrawer(item.configId)">
<v-icon size="18">mdi-arrow-top-right-thick</v-icon>
</v-btn>
</div>
<small v-if="configInfoList.findIndex(c => c.id === item.configId) === -1" style="color: red;"
class="ml-2">{{ tm('createDialog.configMissing') }}</small>
</template>
<template v-slot:item.actions="{ item, index }">
<div v-if="isEditingRoutes" class="d-flex align-center">
<v-btn icon size="x-small" variant="text" @click="moveRouteUp(index)" :disabled="index === 0">
<v-icon>mdi-arrow-up</v-icon>
</v-btn>
<v-btn icon size="x-small" variant="text" @click="moveRouteDown(index)"
:disabled="index === platformRoutes.length - 1">
<v-icon>mdi-arrow-down</v-icon>
</v-btn>
<v-btn icon size="x-small" variant="text" color="error" @click="deleteRoute(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
<span v-else class="text-grey">-</span>
</template>
</v-data-table>
<small class="ml-2 mt-2 d-block" style="color: grey">{{ tm('createDialog.routeHint') }}</small>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">{{ tm('dialog.cancel') }}</v-btn>
<v-btn :disabled="!canSave" color="primary" v-if="!updatingMode" @click="newPlatform" :loading="loading">{{
tm('dialog.save') }}</v-btn>
<v-btn :disabled="!selectedAbConfId" color="primary" v-else @click="newPlatform" :loading="loading">{{
tm('dialog.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- ID冲突确认对话框 -->
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
<v-card>
<v-card-title class="text-h6 bg-warning d-flex align-center">
<v-icon start class="me-2">mdi-alert-circle-outline</v-icon>
{{ tm('dialog.idConflict.title') }}
</v-card-title>
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
{{ tm('dialog.idConflict.message', { id: conflictId }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 安全警告对话框 -->
<v-dialog v-model="showOneBotEmptyTokenWarnDialog" max-width="600" persistent>
<v-card>
<v-card-title>
{{ tm('dialog.securityWarning.title') }}
</v-card-title>
<v-card-text class="py-4">
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
<span><a
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-spacer></v-spacer>
<v-btn color="error" @click="handleOneBotEmptyTokenWarningDismiss(true)">
{{ tm('createDialog.warningContinue') }}
</v-btn>
<v-btn color="primary" @click="handleOneBotEmptyTokenWarningDismiss(false)">
{{ tm('createDialog.warningEditAgain') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-overlay
v-model="showConfigDrawer"
class="config-drawer-overlay"
location="right"
transition="slide-x-reverse-transition"
:scrim="true"
@click:outside="closeConfigDrawer"
>
<v-card class="config-drawer-card" elevation="12">
<div class="config-drawer-header">
<div>
<span class="text-h6">{{ tm('createDialog.configDrawerTitle') }}</span>
<div v-if="configDrawerTargetId" class="text-caption text-grey">
{{ tm('createDialog.configDrawerIdLabel') }}: {{ configDrawerTargetId }}
</div>
</div>
<v-btn icon variant="text" @click="closeConfigDrawer">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-divider></v-divider>
<div class="config-drawer-content">
<ConfigPage v-if="showConfigDrawer" :initial-config-id="configDrawerTargetId" />
</div>
</v-card>
</v-overlay>
</template>
<script>
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
import { getPlatformIcon, getPlatformDescription, getTutorialLink } from '@/utils/platformUtils';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
import ConfigPage from '@/views/ConfigPage.vue';
export default {
name: 'AddNewPlatform',
components: { AstrBotConfig, AstrBotCoreConfigWrapper, ConfigPage },
emits: ['update:show', 'show-toast', 'refresh-config'],
props: {
show: {
type: Boolean,
default: false
},
metadata: {
type: Object,
default: () => ({})
},
config_data: {
type: Object,
default: () => ({})
},
updatingMode: {
type: Boolean,
default: false
},
updatingPlatformConfig: {
type: Object,
default: null
}
},
data() {
return {
selectedPlatformType: null,
selectedPlatformConfig: null,
aBConfigRadioVal: '0',
selectedAbConfId: 'default',
configInfoList: [],
// 选中的配置文件预览数据
selectedConfigData: null,
selectedConfigMetadata: null,
configPreviewLoading: false,
// 新配置文件相关数据
newConfigData: null,
newConfigMetadata: null,
newConfigLoading: false,
// 平台配置文件表格(已弃用,改用 platformRoutes)
platformConfigs: [],
// 平台路由表
platformRoutes: [],
isEditingRoutes: false, // 编辑模式开关
// ID冲突确认对话框
showIdConflictDialog: false,
conflictId: '',
idConflictResolve: null,
// OneBot Empty Token Warning #2639
showOneBotEmptyTokenWarnDialog: false,
oneBotEmptyTokenWarningResolve: null,
loading: false,
showConfigSection: false,
// 配置抽屉
showConfigDrawer: false,
configDrawerTargetId: null,
// 保存更新前的平台 ID,防止用户修改 ID 后丢失原始定位
originalUpdatingPlatformId: null,
};
},
setup() {
const { tm } = useModuleI18n('features/platform');
return { tm };
},
computed: {
showDialog: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
},
platformTemplates() {
return this.metadata['platform_group']?.metadata?.platform?.config_template || {};
},
canSave() {
// 基本条件:必须选择平台类型
if (!this.selectedPlatformType) {
return false;
}
if (!this.isPlatformIdValid(this.selectedPlatformConfig?.id)) {
return false;
}
// 如果是使用现有配置文件模式
if (this.aBConfigRadioVal === '0') {
return !!this.selectedAbConfId;
}
// 如果是创建新配置文件模式
if (this.aBConfigRadioVal === '1') {
// 需要配置文件名称,且新配置数据已加载
return !!(this.selectedAbConfId && this.newConfigData);
}
return false;
},
configTableHeaders() {
return [
{ title: this.tm('createDialog.configTableHeaders.configId'), key: 'name', sortable: false },
{ title: this.tm('createDialog.configTableHeaders.scope'), key: 'scope', sortable: false },
];
},
routeTableHeaders() {
return [
{ title: this.tm('createDialog.routeTableHeaders.source'), key: 'source', sortable: false, width: '60%' },
{ title: this.tm('createDialog.routeTableHeaders.config'), key: 'configId', sortable: false, width: '20%' },
{ title: this.tm('createDialog.routeTableHeaders.actions'), key: 'actions', sortable: false, align: 'center', width: '20%' },
];
},
messageTypeOptions() {
return [
{ label: this.tm('createDialog.messageTypeOptions.all'), value: '*' },
{ label: this.tm('createDialog.messageTypeOptions.group'), value: 'GroupMessage' },
{ label: this.tm('createDialog.messageTypeOptions.friend'), value: 'FriendMessage' },
];
}
},
watch: {
selectedPlatformType(newType) {
if (newType && this.platformTemplates[newType]) {
this.selectedPlatformConfig = JSON.parse(JSON.stringify(this.platformTemplates[newType]));
} else {
this.selectedPlatformConfig = null;
}
},
selectedAbConfId(newConfigId) {
// 当选择配置文件改变时,获取配置文件数据用于预览
if (!this.updatingMode && this.aBConfigRadioVal === '0' && newConfigId) {
this.getConfigForPreview(newConfigId);
} else {
this.selectedConfigData = null;
this.selectedConfigMetadata = null;
}
},
aBConfigRadioVal(newVal) {
// 当切换到创建新配置文件时,获取默认配置模板
if (newVal === '1') {
this.selectedConfigData = null;
this.selectedConfigMetadata = null;
this.selectedAbConfId = null;
this.getDefaultConfigTemplate();
} else if (newVal === '0') {
// 如果切换回使用现有配置文件但没有选择配置文件,重置为默认
this.newConfigData = null;
this.newConfigMetadata = null;
if (!this.selectedAbConfId) {
this.selectedAbConfId = 'default';
}
}
},
showIdConflictDialog(newValue) {
if (!newValue && this.idConflictResolve) {
this.idConflictResolve(false);
this.idConflictResolve = null;
}
},
showOneBotEmptyTokenWarnDialog(newValue) {
if (!newValue && this.oneBotEmptyTokenWarningResolve) {
this.oneBotEmptyTokenWarningResolve(true);
this.oneBotEmptyTokenWarningResolve = null;
}
},
// 监听更新模式变化,获取相关配置文件
updatingPlatformConfig: {
handler(newConfig) {
if (this.updatingMode && newConfig && newConfig.id) {
this.originalUpdatingPlatformId = newConfig.id;
this.getPlatformConfigs(newConfig.id);
}
},
immediate: true
},
showConfigSection(newValue) {
if (newValue && !this.updatingMode && this.aBConfigRadioVal === '0') {
this.getConfigForPreview(this.selectedAbConfId);
}
if (newValue) {
this.$nextTick(() => {
this.scrollDialogToBottom();
});
}
},
// 监听编辑模式变化,自动展开配置文件部分
updatingMode: {
handler(newValue) {
if (newValue) {
this.showConfigSection = true;
// 编辑模式下默认不开启路由编辑模式,用户需要手动点击
this.isEditingRoutes = false;
}
},
immediate: true
}
},
methods: {
getPlatformIcon(platformType) {
// Check for plugin-provided logo_token first
const template = this.platformTemplates?.[platformType];
if (template && template.logo_token) {
return `/api/file/${template.logo_token}`;
}
return getPlatformIcon(platformType);
},
getPlatformDescription,
resetForm() {
this.selectedPlatformType = null;
this.selectedPlatformConfig = null;
this.aBConfigRadioVal = '0';
this.selectedAbConfId = 'default';
// 重置配置预览数据
this.selectedConfigData = null;
this.selectedConfigMetadata = null;
this.configPreviewLoading = false;
// 重置新配置文件数据
this.newConfigData = null;
this.newConfigMetadata = null;
this.newConfigLoading = false;
this.showConfigSection = false;
this.isEditingRoutes = false; // 重置编辑模式
this.showConfigDrawer = false;
this.configDrawerTargetId = null;
this.originalUpdatingPlatformId = null;
},
closeDialog() {
this.resetForm();
this.showDialog = false;
},
async getConfigInfoList() {
await axios.get('/api/config/abconfs').then((res) => {
this.configInfoList = res.data.data.info_list;
})
},
// 获取配置文件数据用于预览
async getConfigForPreview(configId) {
if (!configId) {
this.selectedConfigData = null;
this.selectedConfigMetadata = null;
return;
}
this.configPreviewLoading = true;
try {
const response = await axios.get('/api/config/abconf', {
params: { id: configId }
});
this.selectedConfigData = response.data.data.config;
this.selectedConfigMetadata = response.data.data.metadata;
} catch (error) {
console.error('获取配置文件预览数据失败:', error);
this.selectedConfigData = null;
this.selectedConfigMetadata = null;
} finally {
this.configPreviewLoading = false;
}
},
// 获取默认配置模板用于创建新配置文件
async getDefaultConfigTemplate() {
this.newConfigLoading = true;
try {
const response = await axios.get('/api/config/default');
this.newConfigData = response.data.data.config;
this.newConfigMetadata = response.data.data.metadata;
} catch (error) {
console.error('获取默认配置模板失败:', error);
this.newConfigData = null;
this.newConfigMetadata = null;
} finally {
this.newConfigLoading = false;
}
},
openTutorial() {
const tutorialUrl = getTutorialLink(this.selectedPlatformConfig.type);
window.open(tutorialUrl, '_blank');
},
openConfigDrawer(configId) {
const targetId = configId || 'default';
if (configId && this.configInfoList.findIndex(c => c.id === configId) === -1) {
this.showError(this.tm('messages.configNotFoundOpenConfig'));
}
this.configDrawerTargetId = targetId;
this.showConfigDrawer = true;
},
closeConfigDrawer() {
this.showConfigDrawer = false;
},
newPlatform() {
this.loading = true;
if (this.updatingMode) {
if (this.updatingPlatformConfig.type === 'aiocqhttp') {
const token = this.updatingPlatformConfig.ws_reverse_token;
if (!token || token.trim() === '') {
this.showOneBotEmptyTokenWarning().then((continueWithWarning) => {
if (continueWithWarning) {
this.updatePlatform();
} else {
this.loading = false;
}
});
return;
}
}
this.updatePlatform();
} else {
this.savePlatform();
}
},
async updatePlatform() {
const id = this.originalUpdatingPlatformId || this.updatingPlatformConfig.id;
if (!id) {
this.loading = false;
this.showError(this.tm('messages.updateMissingPlatformId'));
return;
}
if (!this.isPlatformIdValid(id)) {
this.loading = false;
this.showError(this.tm('dialog.invalidPlatformId'));
return;
}
try {
// 更新平台配置
let resp = await axios.post('/api/config/platform/update', {
id: id,
config: this.updatingPlatformConfig
})
if (resp.data.status === 'error') {
throw new Error(resp.data.message || this.tm('messages.platformUpdateFailed'));
}
// 同时更新路由表
await this.saveRoutesInternal();
this.loading = false;
this.showDialog = false;
this.resetForm();
this.$emit('refresh-config');
this.showSuccess(this.tm('messages.updateSuccess'));
} catch (err) {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
}
},
async savePlatform() {
if (!this.isPlatformIdValid(this.selectedPlatformConfig?.id)) {
this.loading = false;
this.showError(this.tm('dialog.invalidPlatformId'));
return;
}
// 检查 ID 是否已存在
const existingPlatform = this.config_data.platform?.find(p => p.id === this.selectedPlatformConfig.id);
if (existingPlatform || this.selectedPlatformConfig.id === 'webchat') {
const confirmed = await this.confirmIdConflict(this.selectedPlatformConfig.id);
if (!confirmed) {
this.loading = false;
return; // 如果用户取消,则中止保存
}
}
// 检查 aiocqhttp 适配器的安全设置
if (this.selectedPlatformConfig.type === 'aiocqhttp') {
const token = this.selectedPlatformConfig.ws_reverse_token;
if (!token || token.trim() === '') {
const continueWithWarning = await this.showOneBotEmptyTokenWarning();
if (!continueWithWarning) {
return;
}
}
}
try {
// 先保存平台配置
const res = await axios.post('/api/config/platform/new', this.selectedPlatformConfig);
// 平台保存成功后,处理配置文件
await this.handleConfigFile();
this.loading = false;
this.showDialog = false;
this.resetForm();
this.$emit('refresh-config');
this.showSuccess(res.data.message || this.tm('messages.addSuccessWithConfig'));
} catch (err) {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
}
},
async handleConfigFile() {
if (!this.selectedAbConfId) {
return;
}
const platformId = this.selectedPlatformConfig.id;
// 生成默认的UMOP:平台ID:*:*(表示该平台的所有消息类型和会话)
const newUmop = `${platformId}:*:*`;
let configId = null;
// 第一步:创建或获取配置文件ID
if (this.aBConfigRadioVal === '0') {
// 使用现有配置文件
configId = this.selectedAbConfId;
} else if (this.aBConfigRadioVal === '1') {
// 创建新配置文件
configId = await this.createNewConfigFile(this.selectedAbConfId);
}
if (!configId) {
throw new Error(this.tm('messages.configIdMissing'));
}
// 第二步:统一更新路由表
await this.updateRoutingTable(newUmop, configId);
},
async updateRoutingTable(umop, configId) {
try {
await axios.post('/api/config/umo_abconf_route/update', {
umo: umop,
conf_id: configId
});
console.log(`成功更新路由表: ${umop} -> ${configId}`);
} catch (err) {
console.error('更新路由表失败:', err);
const errorMessage = err.response?.data?.message || err.message;
throw new Error(this.tm('messages.routingUpdateFailed', { message: errorMessage }));
}
},
async createNewConfigFile(configName) {
try {
// 准备配置数据,如果是创建模式且有新配置数据,使用用户填写的配置
const configData = this.aBConfigRadioVal === '1' && this.newConfigData
? this.newConfigData
: undefined;
// 创建新的配置文件(不传入umop)
const createRes = await axios.post('/api/config/abconf/new', {
name: configName,
config: configData // 传入用户配置的数据
});
const newConfigId = createRes.data.data.conf_id;
console.log(`成功创建新配置文件 ${configName},ID: ${newConfigId}`);
return newConfigId;
} catch (err) {
console.error('创建新配置文件失败:', err);
const errorMessage = err.response?.data?.message || err.message;
throw new Error(this.tm('messages.createConfigFailed', { message: errorMessage }));
}
},
confirmIdConflict(id) {
this.conflictId = id;
this.showIdConflictDialog = true;
return new Promise((resolve) => {
this.idConflictResolve = resolve;
});
},
handleIdConflictConfirm(confirmed) {
if (this.idConflictResolve) {
this.idConflictResolve(confirmed);
}
this.showIdConflictDialog = false;
},
showOneBotEmptyTokenWarning() {
this.showOneBotEmptyTokenWarnDialog = true;
return new Promise((resolve) => {
this.oneBotEmptyTokenWarningResolve = resolve;
});
},
handleOneBotEmptyTokenWarningDismiss(continueWithWarning) {
this.showOneBotEmptyTokenWarnDialog = false;
if (this.oneBotEmptyTokenWarningResolve) {
this.oneBotEmptyTokenWarningResolve(continueWithWarning);
this.oneBotEmptyTokenWarningResolve = null;
}
if (!continueWithWarning) {
this.loading = false;
}
},
showSuccess(message) {
this.$emit('show-toast', { message: message, type: 'success' });
},
showError(message) {
this.$emit('show-toast', { message: message, type: 'error' });
},
isPlatformIdValid(id) {
if (!id) {
return false;
}
return !/[!:]/.test(id);
},
// 获取该平台适配器使用的所有配置文件(新版本:直接操作路由表)
async getPlatformConfigs(platformId) {
if (!platformId) {
this.platformRoutes = [];
return;
}
try {
// 获取路由表 (UMOP -> conf_id)
const routesRes = await axios.get('/api/config/umo_abconf_routes');
const routingTable = routesRes.data.data.routing;
// 过滤出属于该平台的路由,并保持顺序
const routes = [];
for (const [umop, confId] of Object.entries(routingTable)) {
if (this.isUmopMatchPlatform(umop, platformId)) {
const parts = umop.split(':');
if (parts.length === 3) {
routes.push({
umop: umop,
originalUmop: umop, // 保存原始 UMOP 用于更新时查找
messageType: parts[1] === '' || parts[1] === '*' ? '*' : parts[1],
sessionId: parts[2] === '' || parts[2] === '*' ? '*' : parts[2],
configId: confId
});
}
}
}
this.platformRoutes = routes;
// 如果没有路由,添加一个默认的空路由供用户编辑
if (this.platformRoutes.length === 0) {
this.platformRoutes.push({
umop: null,
originalUmop: null,
messageType: '*',
sessionId: '*',
configId: 'default'
});
}
} catch (err) {
console.error('获取平台路由配置失败:', err);
this.platformRoutes = [];
}
},
// 添加新路由
addNewRoute() {
this.platformRoutes.push({
umop: null,
originalUmop: null,
messageType: '*',
sessionId: '*',
configId: 'default'
});
},
// 删除路由
deleteRoute(index) {
this.platformRoutes.splice(index, 1);
},
// 上移路由
moveRouteUp(index) {
if (index > 0) {
const temp = this.platformRoutes[index];
this.platformRoutes[index] = this.platformRoutes[index - 1];
this.platformRoutes[index - 1] = temp;
// 强制更新视图
this.platformRoutes = [...this.platformRoutes];
}
},
// 下移路由
moveRouteDown(index) {
if (index < this.platformRoutes.length - 1) {
const temp = this.platformRoutes[index];
this.platformRoutes[index] = this.platformRoutes[index + 1];
this.platformRoutes[index + 1] = temp;
// 强制更新视图
this.platformRoutes = [...this.platformRoutes];
}
},
// 内部保存路由表方法(不显示成功提示)
async saveRoutesInternal() {
const originalPlatformId = this.originalUpdatingPlatformId || this.updatingPlatformConfig?.id;
const newPlatformId = this.updatingPlatformConfig?.id || originalPlatformId;
if (!originalPlatformId && !newPlatformId) {
throw new Error(this.tm('messages.platformIdMissing'));
}
try {
// 获取完整的路由表
const routesRes = await axios.get('/api/config/umo_abconf_routes');
const fullRoutingTable = routesRes.data.data.routing;
// 删除该平台的所有旧路由
for (const umop in fullRoutingTable) {
if (
(originalPlatformId && this.isUmopMatchPlatform(umop, originalPlatformId)) ||
(newPlatformId && this.isUmopMatchPlatform(umop, newPlatformId))
) {
delete fullRoutingTable[umop];
}
}
// 添加新路由(按顺序)
for (const route of this.platformRoutes) {
const messageType = route.messageType === '*' ? '*' : route.messageType;
const sessionId = route.sessionId === '*' ? '*' : route.sessionId;
const platformIdForRoute = newPlatformId || originalPlatformId;
const newUmop = `${platformIdForRoute}:${messageType}:${sessionId}`;
if (route.configId) {
fullRoutingTable[newUmop] = route.configId;
}
}
// 使用 update_all 更新整个路由表
await axios.post('/api/config/umo_abconf_route/update_all', {
routing: fullRoutingTable
});
} catch (err) {
console.error('保存路由表失败:', err);
const errorMessage = err.response?.data?.message || err.message;
throw new Error(this.tm('messages.routingSaveFailed', { message: errorMessage }));
}
},
// 切换编辑模式
toggleEditMode() {
this.isEditingRoutes = !this.isEditingRoutes;
},
toggleConfigSection() {
this.showConfigSection = !this.showConfigSection;
},
// 根据配置文件ID获取名称
getConfigName(configId) {
const config = this.configInfoList.find(c => c.id === configId);
return config ? config.name : configId;
},
isUmopMatchPlatform(umop, platformId) {
if (!umop) return false;
const parts = umop.split(':');
if (parts.length !== 3) return false;
const platform = parts[0];
return platform === platformId || platform === '' || platform === '*';
},
// 获取消息类型标签
getMessageTypeLabel(messageType) {
const typeMap = {
'*': this.tm('createDialog.messageTypeLabels.all'),
'': this.tm('createDialog.messageTypeLabels.all'),
'GroupMessage': this.tm('createDialog.messageTypeLabels.group'),
'FriendMessage': this.tm('createDialog.messageTypeLabels.friend')
};
return typeMap[messageType] || messageType;
},
toggleShowConfigSection() {
this.showConfigSection = false;
this.showConfigSection = true;
},
prepareData() {
this.getConfigInfoList();
this.getConfigForPreview(this.selectedAbConfId);
if (this.updatingMode && this.updatingPlatformConfig && this.updatingPlatformConfig.id) {
this.getPlatformConfigs(this.updatingPlatformConfig.id);
}
},
scrollDialogToBottom() {
const containerRef = this.$refs.dialogScrollContainer;
const el = containerRef?.$el || containerRef;
if (!el) {
return;
}
const scrollOptions = { top: el.scrollHeight, behavior: 'smooth' };
if (typeof el.scrollTo === 'function') {
el.scrollTo(scrollOptions);
} else {
el.scrollTop = el.scrollHeight;
}
}
},
}
</script>
<style>
.v-select__selection-text {
font-size: 12px;
}
.config-drawer-overlay {
align-items: stretch;
justify-content: flex-end;
}
.config-drawer-card {
width: clamp(320px, 60vw, 820px);
height: calc(100vh - 32px);
display: flex;
flex-direction: column;
margin: 16px;
}
.config-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px 20px;
}
.config-drawer-content {
flex: 1;
overflow-y: auto;
padding: 16px 16px 24px 16px;
}
</style>