Files
WebAI2API/webui/src/components/settings/workers.vue
T

491 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>