mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
refactor: 代码解耦合,完善WebUI功能,添加网页VNC
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SettingOutlined, AppstoreOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const currentAdapter = ref(null);
|
||||
const currentConfig = reactive({});
|
||||
|
||||
// 挂载时获取数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
settingsStore.fetchAdaptersMeta(),
|
||||
settingsStore.fetchAdapterConfig()
|
||||
]);
|
||||
});
|
||||
|
||||
// 适配器列表
|
||||
const adapters = computed(() => settingsStore.adaptersMeta);
|
||||
|
||||
// 打开抽屉进行编辑
|
||||
const handleEdit = (adapter) => {
|
||||
currentAdapter.value = adapter;
|
||||
// 加载现有配置或默认值
|
||||
const existing = settingsStore.adapterConfig[adapter.id] || {};
|
||||
|
||||
// 重置当前配置表单
|
||||
Object.keys(currentConfig).forEach(key => delete currentConfig[key]);
|
||||
|
||||
// 使用现有值或schema中的默认值初始化表单
|
||||
if (adapter.configSchema) {
|
||||
adapter.configSchema.forEach(field => {
|
||||
if (existing[field.key] !== undefined) {
|
||||
currentConfig[field.key] = existing[field.key];
|
||||
} else {
|
||||
currentConfig[field.key] = field.default;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
if (!currentAdapter.value) return;
|
||||
|
||||
const configToSave = {
|
||||
[currentAdapter.value.id]: { ...currentConfig }
|
||||
};
|
||||
|
||||
const success = await settingsStore.saveAdapterConfig(configToSave);
|
||||
if (success) {
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout style="background: transparent;">
|
||||
<a-card title="适配器管理" :bordered="false">
|
||||
<template #extra>
|
||||
<a-button type="link" @click="settingsStore.fetchAdaptersMeta">刷新列表</a-button>
|
||||
</template>
|
||||
|
||||
<a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }" :data-source="adapters">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '16px' }">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<AppstoreOutlined style="font-size: 20px; color: #1890ff; margin-right: 12px;" />
|
||||
<span style="font-weight: 600; font-size: 15px;">{{ item.id }}</span>
|
||||
</div>
|
||||
<SettingOutlined style="font-size: 16px; color: #8c8c8c;" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
|
||||
<!-- 配置抽屉 -->
|
||||
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.name}`"
|
||||
width="500" placement="right">
|
||||
<div v-if="!currentAdapter.configSchema || currentAdapter.configSchema.length === 0">
|
||||
<a-empty description="该适配器没有可配置项" />
|
||||
</div>
|
||||
|
||||
<a-form layout="vertical" v-else>
|
||||
<template v-for="field in currentAdapter.configSchema" :key="field.key">
|
||||
<a-form-item :label="field.label" :required="field.required">
|
||||
<!-- 字符串输入 -->
|
||||
<a-input v-if="field.type === 'string'" v-model:value="currentConfig[field.key]"
|
||||
:placeholder="field.placeholder" />
|
||||
|
||||
<!-- 数字输入 -->
|
||||
<a-input-number v-if="field.type === 'number'" v-model:value="currentConfig[field.key]"
|
||||
:min="field.min" :max="field.max" style="width: 100%;" />
|
||||
|
||||
<!-- 布尔开关 -->
|
||||
<div v-if="field.type === 'boolean'">
|
||||
<a-switch v-model:checked="currentConfig[field.key]" />
|
||||
</div>
|
||||
|
||||
<!-- 下拉选择 -->
|
||||
<a-select v-if="field.type === 'select'" v-model:value="currentConfig[field.key]"
|
||||
:options="field.options" />
|
||||
|
||||
<div v-if="field.note" style="font-size: 12px; color: #8c8c8c; margin-top: 4px;">
|
||||
{{ field.note }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<div style="text-align: right;">
|
||||
<a-button style="margin-right: 8px" @click="drawerVisible = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存配置</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</a-layout>
|
||||
</template>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
path: '',
|
||||
headless: false,
|
||||
// 全局代理
|
||||
proxyEnable: false,
|
||||
proxyType: 'http',
|
||||
proxyHost: '127.0.0.1',
|
||||
proxyPort: 7890,
|
||||
proxyAuth: false,
|
||||
proxyUser: '',
|
||||
proxyPasswd: ''
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await settingsStore.fetchBrowserConfig();
|
||||
const cfg = settingsStore.browserConfig || {};
|
||||
formData.path = cfg.path || '';
|
||||
formData.headless = cfg.headless || false;
|
||||
|
||||
if (cfg.proxy) {
|
||||
formData.proxyEnable = cfg.proxy.enable || false;
|
||||
formData.proxyType = cfg.proxy.type || 'http';
|
||||
formData.proxyHost = cfg.proxy.host || '';
|
||||
formData.proxyPort = cfg.proxy.port || 7890;
|
||||
formData.proxyAuth = cfg.proxy.auth || false;
|
||||
formData.proxyUser = cfg.proxy.username || '';
|
||||
formData.proxyPasswd = cfg.proxy.password || '';
|
||||
}
|
||||
});
|
||||
|
||||
// 保存设置
|
||||
const handleSave = async () => {
|
||||
const config = {
|
||||
path: formData.path,
|
||||
headless: formData.headless,
|
||||
proxy: {
|
||||
enable: formData.proxyEnable,
|
||||
type: formData.proxyType,
|
||||
host: formData.proxyHost,
|
||||
port: formData.proxyPort,
|
||||
auth: formData.proxyAuth,
|
||||
username: formData.proxyUser,
|
||||
password: formData.proxyPasswd
|
||||
}
|
||||
};
|
||||
await settingsStore.saveBrowserConfig(config);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout style="background: transparent;">
|
||||
<a-card title="浏览器设置" :bordered="false" style="width: 100%;">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<!-- 浏览器可执行文件路径 -->
|
||||
<a-col :xs="24" :md="24">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">浏览器可执行文件路径</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
留空则使用 Camoufox 默认下载路径<br>
|
||||
Windows示例: C:\camoufox\camoufox.exe<br>
|
||||
Linux示例: /opt/camoufox/camoufox
|
||||
</div>
|
||||
<a-input v-model:value="formData.path" placeholder="留空使用默认路径" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 无头模式 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">无头模式</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
启用后浏览器将在后台运行,不显示窗口
|
||||
</div>
|
||||
<a-switch v-model:checked="formData.headless" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ formData.headless ? '已启用' : '未启用' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 全局代理设置(折叠面板) -->
|
||||
<div style="margin-top: 16px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel key="proxy" header="全局代理设置">
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 16px;">
|
||||
如果实例没有独立配置代理,将使用此全局代理配置
|
||||
</div>
|
||||
|
||||
<!-- 是否启用代理 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-switch v-model:checked="formData.proxyEnable" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ formData.proxyEnable ? '已启用全局代理' : '未启用全局代理' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 代理类型 -->
|
||||
<div style="margin-bottom: 16px;" v-if="formData.proxyEnable">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理类型</div>
|
||||
<a-segmented v-model:value="formData.proxyType" block :options="[
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'SOCKS5', value: 'socks5' }
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<a-row :gutter="16" v-if="formData.proxyEnable">
|
||||
<!-- 代理主机 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理主机</div>
|
||||
<a-input v-model:value="formData.proxyHost" placeholder="例如: 127.0.0.1" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 代理端口 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理端口</div>
|
||||
<a-input-number v-model:value="formData.proxyPort" :min="1" :max="65535"
|
||||
style="width: 100%" placeholder="例如: 7890" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 是否需要验证 -->
|
||||
<div style="margin-bottom: 16px;" v-if="formData.proxyEnable">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理认证</div>
|
||||
<a-switch v-model:checked="formData.proxyAuth" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ formData.proxyAuth ? '需要认证' : '无需认证' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="16" v-if="formData.proxyEnable && formData.proxyAuth">
|
||||
<!-- 用户名 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">用户名</div>
|
||||
<a-input v-model:value="formData.proxyUser" placeholder="请输入用户名" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 密码 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">密码</div>
|
||||
<a-input-password v-model:value="formData.proxyPasswd" placeholder="请输入密码" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮(右下角) -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
|
||||
<a-button type="primary" @click="handleSave">
|
||||
保存设置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-layout>
|
||||
</template>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
port: 5173,
|
||||
authToken: '',
|
||||
keepaliveMode: 'comment',
|
||||
queueBuffer: 2,
|
||||
imageLimit: 5
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await settingsStore.fetchServerConfig();
|
||||
Object.assign(formData, settingsStore.serverConfig);
|
||||
});
|
||||
|
||||
// 保存设置
|
||||
const handleSave = async () => {
|
||||
await settingsStore.saveServerConfig(formData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout style="background: transparent;">
|
||||
<a-card title="服务器设置" :bordered="false" style="width: 100%;">
|
||||
<!-- 4宫格表单布局 -->
|
||||
<a-row :gutter="[16, 16]">
|
||||
<!-- 监听端口 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">监听端口</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
设置服务器监听的端口号,默认为 5173
|
||||
</div>
|
||||
<a-input-number v-model:value="formData.port" :min="1" :max="65535" placeholder="请输入端口号"
|
||||
style="width: 100%" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 鉴权 Token -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">鉴权 Token</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
用于 API 请求鉴权的密钥,留空则不启用鉴权
|
||||
</div>
|
||||
<a-input-password v-model:value="formData.authToken" placeholder="请输入 Token" type="password" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 心跳包类型 (Keepalive Mode) -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">心跳包类型</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
选择 SSE 流式响应的心跳包格式
|
||||
</div>
|
||||
<a-select v-model:value="formData.keepaliveMode" style="width: 100%" placeholder="请选择心跳包类型">
|
||||
<a-select-option value="comment">Comment - 注释格式</a-select-option>
|
||||
<a-select-option value="content">Content - 内容格式</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 保存按钮(右下角) -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
|
||||
<a-button type="primary" @click="handleSave">
|
||||
保存设置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 队列设置 -->
|
||||
<a-card title="队列设置" :bordered="false" style="width: 100%; margin-top: 10px;">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<!-- 队列缓冲区大小 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">队列缓冲区大小</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
非流式请求的额外排队数(设为 0 则不限制非流式请求数量)<br>
|
||||
实际队列上限 = Workers数量 + 缓冲区大小
|
||||
</div>
|
||||
<a-input-number v-model:value="formData.queueBuffer" :min="0" :max="100" placeholder="默认为 2"
|
||||
style="width: 100%" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 图片数量上限 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">图片数量上限</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
单次请求最多支持的图片附件数量<br>
|
||||
网页最多支持10个附件,超出会被丢弃
|
||||
</div>
|
||||
<a-input-number v-model:value="formData.imageLimit" :min="1" :max="10" placeholder="默认为 5"
|
||||
style="width: 100%" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 保存按钮(右下角) -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
|
||||
<a-button type="primary" @click="handleSave">
|
||||
保存设置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 确保在手机端也能正常显示 */
|
||||
.ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,490 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const poolConfig = computed({
|
||||
get: () => settingsStore.poolConfig,
|
||||
set: (val) => settingsStore.poolConfig = val
|
||||
});
|
||||
|
||||
const handleSavePool = async () => {
|
||||
await settingsStore.savePoolConfig(poolConfig.value);
|
||||
};
|
||||
|
||||
// 获取初始数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
settingsStore.fetchWorkerConfig(),
|
||||
settingsStore.fetchPoolConfig(),
|
||||
settingsStore.fetchAdaptersMeta()
|
||||
]);
|
||||
});
|
||||
|
||||
// 计算属性:适配器选项
|
||||
const adapterOptions = computed(() => {
|
||||
const options = settingsStore.adaptersMeta.map(a => ({
|
||||
label: a.name,
|
||||
value: a.id
|
||||
}));
|
||||
// 手动添加 Merge 选项(如果没有在元数据中返回)
|
||||
if (!options.find(o => o.value === 'merge')) {
|
||||
options.push({ label: 'Merge(聚合模式)', value: 'merge' });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
// 实例列表表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '实例名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Worker 数量',
|
||||
dataIndex: 'workerCount',
|
||||
key: 'workerCount',
|
||||
},
|
||||
{
|
||||
title: '代理',
|
||||
dataIndex: 'proxy',
|
||||
key: 'proxy',
|
||||
},
|
||||
{
|
||||
title: '数据标记',
|
||||
key: 'userDataMark',
|
||||
dataIndex: 'userDataMark',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
// 实例列表数据 (从 Store 获取)
|
||||
const instanceData = computed({
|
||||
get: () => settingsStore.workerConfig,
|
||||
set: (val) => { settingsStore.workerConfig = val; }
|
||||
});
|
||||
|
||||
// 抽屉状态
|
||||
const drawerOpen = ref(false);
|
||||
const editingInstance = ref(null);
|
||||
|
||||
// 编辑表单数据
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
userDataMark: '',
|
||||
proxy: false,
|
||||
proxyType: 'socks5',
|
||||
proxyHost: '',
|
||||
proxyPort: 1080,
|
||||
proxyAuth: false,
|
||||
proxyUsername: '',
|
||||
proxyPassword: '',
|
||||
workers: []
|
||||
});
|
||||
|
||||
// 创建实例
|
||||
const handleCreateInstance = () => {
|
||||
editingInstance.value = null; // null表示创建新实例
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
||||
// 重置表单为默认值
|
||||
editForm.value = {
|
||||
name: `instance-${(instanceData.value || []).length + 1}-${randomSuffix}`,
|
||||
userDataMark: '',
|
||||
proxy: false,
|
||||
proxyType: 'socks5',
|
||||
proxyHost: '',
|
||||
proxyPort: 1080,
|
||||
proxyAuth: false,
|
||||
proxyUsername: '',
|
||||
proxyPassword: '',
|
||||
workers: []
|
||||
};
|
||||
drawerOpen.value = true;
|
||||
};
|
||||
|
||||
// 编辑实例
|
||||
const handleEdit = (record) => {
|
||||
editingInstance.value = record;
|
||||
// 填充表单数据
|
||||
editForm.value = {
|
||||
name: record.name,
|
||||
userDataMark: record.userDataMark || '',
|
||||
proxy: record.proxy ? true : false,
|
||||
proxyType: record.proxy?.type || 'socks5',
|
||||
proxyHost: record.proxy?.host || '',
|
||||
proxyPort: record.proxy?.port || 1080,
|
||||
proxyAuth: record.proxy?.auth || false,
|
||||
proxyUsername: record.proxy?.username || '',
|
||||
proxyPassword: record.proxy?.password || '',
|
||||
workers: record.workers ? [...record.workers] : []
|
||||
};
|
||||
// 兼容前端展示用的 proxy 布尔值
|
||||
if (record.proxy === null || record.proxy === undefined) {
|
||||
editForm.value.proxy = false;
|
||||
}
|
||||
drawerOpen.value = true;
|
||||
};
|
||||
|
||||
// 删除实例
|
||||
const handleDelete = async (record) => {
|
||||
const newList = instanceData.value.filter(item => item.id !== record.id);
|
||||
await settingsStore.saveWorkerConfig(newList);
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const handleSaveEdit = async () => {
|
||||
// 构建要保存的对象结构
|
||||
const instanceToSave = {
|
||||
id: editingInstance.value ? editingInstance.value.id : `inst_${Date.now()}`,
|
||||
name: editForm.value.name,
|
||||
userDataMark: editForm.value.userDataMark,
|
||||
workers: editForm.value.workers,
|
||||
// 如果启用了代理,则构建代理对象,否则为 null
|
||||
proxy: editForm.value.proxy ? {
|
||||
enable: true,
|
||||
type: editForm.value.proxyType,
|
||||
host: editForm.value.proxyHost,
|
||||
port: editForm.value.proxyPort,
|
||||
auth: editForm.value.proxyAuth,
|
||||
username: editForm.value.proxyUsername,
|
||||
password: editForm.value.proxyPassword
|
||||
} : null
|
||||
};
|
||||
|
||||
let newList = [...(instanceData.value || [])];
|
||||
if (editingInstance.value === null) {
|
||||
// 创建
|
||||
newList.push(instanceToSave);
|
||||
} else {
|
||||
// 更新
|
||||
const index = newList.findIndex(item => item.id === editingInstance.value.id);
|
||||
if (index > -1) {
|
||||
newList[index] = instanceToSave;
|
||||
}
|
||||
}
|
||||
|
||||
const success = await settingsStore.saveWorkerConfig(newList);
|
||||
if (success) {
|
||||
drawerOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑中的Worker索引
|
||||
const editingWorkerIndex = ref(-1);
|
||||
const workerFormVisible = ref(false);
|
||||
const workerForm = ref({
|
||||
name: '',
|
||||
type: 'lmarena',
|
||||
mergeTypes: [],
|
||||
mergeMonitor: ''
|
||||
});
|
||||
|
||||
// 添加Worker
|
||||
const handleAddWorker = () => {
|
||||
editingWorkerIndex.value = -1;
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
||||
workerForm.value = {
|
||||
name: `worker-${editForm.value.workers.length + 1}-${randomSuffix}`,
|
||||
type: 'lmarena',
|
||||
mergeTypes: [],
|
||||
mergeMonitor: ''
|
||||
};
|
||||
workerFormVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑Worker
|
||||
const handleEditWorker = (index) => {
|
||||
editingWorkerIndex.value = index;
|
||||
const worker = editForm.value.workers[index];
|
||||
workerForm.value = {
|
||||
name: worker.name,
|
||||
type: worker.type,
|
||||
mergeTypes: worker.mergeTypes ? [...worker.mergeTypes] : [],
|
||||
mergeMonitor: worker.mergeMonitor || ''
|
||||
};
|
||||
workerFormVisible.value = true;
|
||||
};
|
||||
|
||||
// 保存Worker配置
|
||||
const handleSaveWorker = () => {
|
||||
if (editingWorkerIndex.value === -1) {
|
||||
// 新增
|
||||
editForm.value.workers.push({ ...workerForm.value });
|
||||
} else {
|
||||
// 编辑
|
||||
editForm.value.workers[editingWorkerIndex.value] = { ...workerForm.value };
|
||||
}
|
||||
workerFormVisible.value = false;
|
||||
};
|
||||
|
||||
// 删除Worker
|
||||
const handleRemoveWorker = (index) => {
|
||||
editForm.value.workers.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout style="background: transparent;">
|
||||
<a-card title="负载均衡" :bordered="false" style="width: 100%; margin-bottom: 10px;">
|
||||
<!-- 调度策略 -->
|
||||
<div style="margin-bottom: 24px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">调度策略</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
选择任务分配到工作实例的调度算法
|
||||
</div>
|
||||
<a-segmented v-model:value="poolConfig.strategy" block :options="[
|
||||
{ label: '最少繁忙', value: 'least_busy' },
|
||||
{ label: '轮询', value: 'round_robin' },
|
||||
{ label: '随机', value: 'random' }
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<!-- 故障转移 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">故障转移</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
启用后,任务失败时会自动切换到其他可用实例重试
|
||||
</div>
|
||||
<a-switch v-model:checked="poolConfig.failover.enabled" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">重试次数</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
故障转移时最大重试次数,范围 1-10
|
||||
</div>
|
||||
<a-input-number v-model:value="poolConfig.failover.maxRetries" :min="1" :max="10"
|
||||
:disabled="!poolConfig.failover.enabled" style="width: 100%" placeholder="请输入重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
|
||||
<a-button type="primary" @click="handleSavePool">
|
||||
保存设置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
|
||||
<a-card :bordered="false" style="width: 100%;">
|
||||
<!-- 卡片标题和创建按钮 -->
|
||||
<template #title>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>实例列表</span>
|
||||
<a-button type="primary" @click="handleCreateInstance">
|
||||
创建实例
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 实例表格 -->
|
||||
<a-table :columns="columns" :data-source="instanceData" :pagination="false">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 实例名称 -->
|
||||
<template v-if="column.key === 'name'">
|
||||
<a>{{ record.name }}</a>
|
||||
</template>
|
||||
|
||||
<!-- Worker 数量 -->
|
||||
<template v-else-if="column.key === 'workerCount'">
|
||||
{{ record.workers ? record.workers.length : 0 }}
|
||||
</template>
|
||||
|
||||
<!-- 代理状态 -->
|
||||
<template v-else-if="column.key === 'proxy'">
|
||||
<a-tag :color="record.proxy ? 'green' : 'default'">
|
||||
{{ record.proxy ? '已启用' : '未启用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<span>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a style="color: #ff4d4f" @click="handleDelete(record)">删除</a>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑/创建抽屉 -->
|
||||
<a-drawer v-model:open="drawerOpen"
|
||||
:title="editingInstance === null ? '创建实例' : `编辑实例 - ${editingInstance.name}`" placement="right" width="500">
|
||||
<div style="margin-bottom: 24px;">
|
||||
<!-- 实例名称 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">实例名称</div>
|
||||
<div style="font-size: 12px; color: #ff4d4f; margin-bottom: 8px;">
|
||||
* 名称必须全局唯一,不可重复
|
||||
</div>
|
||||
<a-input v-model:value="editForm.name" placeholder="请输入实例名称" />
|
||||
</div>
|
||||
|
||||
<!-- 数据标记 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">数据标记</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
用于区分实例数据存储的文件夹名称 (userDataMark)
|
||||
</div>
|
||||
<a-input v-model:value="editForm.userDataMark" placeholder="请输入数据标记,如: main-gemini" />
|
||||
</div>
|
||||
|
||||
<!-- 代理设置(折叠面板) -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel key="proxy" header="代理设置">
|
||||
<!-- 是否启用代理 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-switch v-model:checked="editForm.proxy" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ editForm.proxy ? '已启用代理' : '未启用代理' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 代理类型 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理类型</div>
|
||||
<a-segmented v-model:value="editForm.proxyType" block :options="[
|
||||
{ label: 'SOCKS5', value: 'socks5' },
|
||||
{ label: 'HTTP', value: 'http' }
|
||||
]" style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- 服务器地址 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">服务器地址</div>
|
||||
<a-input v-model:value="editForm.proxyHost" placeholder="例如: 127.0.0.1" />
|
||||
</div>
|
||||
|
||||
<!-- 端口 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">端口</div>
|
||||
<a-input-number v-model:value="editForm.proxyPort" :min="1" :max="65535"
|
||||
style="width: 100%" placeholder="例如: 1080" />
|
||||
</div>
|
||||
|
||||
<!-- 是否需要验证 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">身份验证</div>
|
||||
<a-switch v-model:checked="editForm.proxyAuth" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ editForm.proxyAuth ? '需要验证' : '无需验证' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy && editForm.proxyAuth">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">用户名</div>
|
||||
<a-input v-model:value="editForm.proxyUsername" placeholder="请输入用户名" />
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div style="margin-bottom: 16px;" v-if="editForm.proxy && editForm.proxyAuth">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">密码</div>
|
||||
<a-input-password v-model:value="editForm.proxyPassword" placeholder="请输入密码" />
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
<!-- Worker 列表 -->
|
||||
<div>
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div style="font-weight: 600;">Worker 列表</div>
|
||||
<a-button size="small" type="primary" @click="handleAddWorker">
|
||||
添加 Worker
|
||||
</a-button>
|
||||
</div>
|
||||
<a-list bordered :data-source="editForm.workers" style="margin-top: 8px;">
|
||||
<template #renderItem="{ item, index }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a @click="handleEditWorker(index)">编辑</a>
|
||||
<a style="color: #ff4d4f" @click="handleRemoveWorker(index)">删除</a>
|
||||
</template>
|
||||
<div>
|
||||
<div style="font-weight: 600;">{{ item.name }}</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c;">
|
||||
类型: {{ item.type }}
|
||||
<span v-if="item.type === 'merge'">
|
||||
| 聚合: {{ item.mergeTypes?.join(', ') || '无' }}
|
||||
<span v-if="item.mergeMonitor">
|
||||
| 监控: {{ item.mergeMonitor }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 抽屉底部保存按钮 -->
|
||||
<template #footer>
|
||||
<div style="text-align: right;">
|
||||
<a-button style="margin-right: 8px" @click="drawerOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSaveEdit">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- Worker配置模态框 -->
|
||||
<a-modal v-model:open="workerFormVisible" :title="editingWorkerIndex === -1 ? '添加 Worker' : '编辑 Worker'"
|
||||
okText="确定" cancelText="取消" @ok="handleSaveWorker">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">Worker 名称</div>
|
||||
<div style="font-size: 12px; color: #ff4d4f; margin-bottom: 8px;">
|
||||
* 名称必须全局唯一,不可重复
|
||||
</div>
|
||||
<a-input v-model:value="workerForm.name" placeholder="例如: default" />
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">适配器类型</div>
|
||||
<a-select v-model:value="workerForm.type" style="width: 100%" :options="adapterOptions" />
|
||||
</div>
|
||||
|
||||
<!-- Merge 模式额外配置 -->
|
||||
<template v-if="workerForm.type === 'merge'">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">聚合类型</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
选择要聚合的后端适配器(可多选)
|
||||
</div>
|
||||
<a-select v-model:value="workerForm.mergeTypes" mode="multiple" style="width: 100%"
|
||||
placeholder="选择要聚合的适配器" :options="adapterOptions">
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">空闲监控后端</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
|
||||
空闲时挂机监控的后端(可选)
|
||||
</div>
|
||||
<a-select v-model:value="workerForm.mergeMonitor" style="width: 100%" placeholder="选择监控后端(可留空)"
|
||||
allow-clear>
|
||||
<a-select-option value="">无</a-select-option>
|
||||
<a-select-option v-for="type in workerForm.mergeTypes" :key="type" :value="type">
|
||||
{{ type }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user