|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useRef, useMemo } from 'react'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { |
|
|
API, |
|
|
showError, |
|
|
showInfo, |
|
|
showSuccess, |
|
|
loadChannelModels, |
|
|
copy, |
|
|
} from '../../helpers'; |
|
|
import { |
|
|
CHANNEL_OPTIONS, |
|
|
ITEMS_PER_PAGE, |
|
|
MODEL_TABLE_PAGE_SIZE, |
|
|
} from '../../constants'; |
|
|
import { useIsMobile } from '../common/useIsMobile'; |
|
|
import { useTableCompactMode } from '../common/useTableCompactMode'; |
|
|
import { Modal } from '@douyinfe/semi-ui'; |
|
|
|
|
|
export const useChannelsData = () => { |
|
|
const { t } = useTranslation(); |
|
|
const isMobile = useIsMobile(); |
|
|
|
|
|
|
|
|
const [channels, setChannels] = useState([]); |
|
|
const [loading, setLoading] = useState(true); |
|
|
const [activePage, setActivePage] = useState(1); |
|
|
const [idSort, setIdSort] = useState(false); |
|
|
const [searching, setSearching] = useState(false); |
|
|
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); |
|
|
const [channelCount, setChannelCount] = useState(0); |
|
|
const [groupOptions, setGroupOptions] = useState([]); |
|
|
|
|
|
|
|
|
const [showEdit, setShowEdit] = useState(false); |
|
|
const [enableBatchDelete, setEnableBatchDelete] = useState(false); |
|
|
const [editingChannel, setEditingChannel] = useState({ id: undefined }); |
|
|
const [showEditTag, setShowEditTag] = useState(false); |
|
|
const [editingTag, setEditingTag] = useState(''); |
|
|
const [selectedChannels, setSelectedChannels] = useState([]); |
|
|
const [enableTagMode, setEnableTagMode] = useState(false); |
|
|
const [showBatchSetTag, setShowBatchSetTag] = useState(false); |
|
|
const [batchSetTagValue, setBatchSetTagValue] = useState(''); |
|
|
const [compactMode, setCompactMode] = useTableCompactMode('channels'); |
|
|
|
|
|
|
|
|
const [visibleColumns, setVisibleColumns] = useState({}); |
|
|
const [showColumnSelector, setShowColumnSelector] = useState(false); |
|
|
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState( |
|
|
localStorage.getItem('channel-status-filter') || 'all', |
|
|
); |
|
|
|
|
|
|
|
|
const [activeTypeKey, setActiveTypeKey] = useState('all'); |
|
|
const [typeCounts, setTypeCounts] = useState({}); |
|
|
|
|
|
|
|
|
const [showModelTestModal, setShowModelTestModal] = useState(false); |
|
|
const [currentTestChannel, setCurrentTestChannel] = useState(null); |
|
|
const [modelSearchKeyword, setModelSearchKeyword] = useState(''); |
|
|
const [modelTestResults, setModelTestResults] = useState({}); |
|
|
const [testingModels, setTestingModels] = useState(new Set()); |
|
|
const [selectedModelKeys, setSelectedModelKeys] = useState([]); |
|
|
const [isBatchTesting, setIsBatchTesting] = useState(false); |
|
|
const [modelTablePage, setModelTablePage] = useState(1); |
|
|
const [selectedEndpointType, setSelectedEndpointType] = useState(''); |
|
|
|
|
|
|
|
|
const shouldStopBatchTestingRef = useRef(false); |
|
|
|
|
|
|
|
|
const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false); |
|
|
const [currentMultiKeyChannel, setCurrentMultiKeyChannel] = useState(null); |
|
|
|
|
|
|
|
|
const requestCounter = useRef(0); |
|
|
const allSelectingRef = useRef(false); |
|
|
const [formApi, setFormApi] = useState(null); |
|
|
|
|
|
const formInitValues = { |
|
|
searchKeyword: '', |
|
|
searchGroup: '', |
|
|
searchModel: '', |
|
|
}; |
|
|
|
|
|
|
|
|
const COLUMN_KEYS = { |
|
|
ID: 'id', |
|
|
NAME: 'name', |
|
|
GROUP: 'group', |
|
|
TYPE: 'type', |
|
|
STATUS: 'status', |
|
|
RESPONSE_TIME: 'response_time', |
|
|
BALANCE: 'balance', |
|
|
PRIORITY: 'priority', |
|
|
WEIGHT: 'weight', |
|
|
OPERATE: 'operate', |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const localIdSort = localStorage.getItem('id-sort') === 'true'; |
|
|
const localPageSize = |
|
|
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; |
|
|
const localEnableTagMode = |
|
|
localStorage.getItem('enable-tag-mode') === 'true'; |
|
|
const localEnableBatchDelete = |
|
|
localStorage.getItem('enable-batch-delete') === 'true'; |
|
|
|
|
|
setIdSort(localIdSort); |
|
|
setPageSize(localPageSize); |
|
|
setEnableTagMode(localEnableTagMode); |
|
|
setEnableBatchDelete(localEnableBatchDelete); |
|
|
|
|
|
loadChannels(1, localPageSize, localIdSort, localEnableTagMode) |
|
|
.then() |
|
|
.catch((reason) => { |
|
|
showError(reason); |
|
|
}); |
|
|
fetchGroups().then(); |
|
|
loadChannelModels().then(); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const getDefaultColumnVisibility = () => { |
|
|
return { |
|
|
[COLUMN_KEYS.ID]: true, |
|
|
[COLUMN_KEYS.NAME]: true, |
|
|
[COLUMN_KEYS.GROUP]: true, |
|
|
[COLUMN_KEYS.TYPE]: true, |
|
|
[COLUMN_KEYS.STATUS]: true, |
|
|
[COLUMN_KEYS.RESPONSE_TIME]: true, |
|
|
[COLUMN_KEYS.BALANCE]: true, |
|
|
[COLUMN_KEYS.PRIORITY]: true, |
|
|
[COLUMN_KEYS.WEIGHT]: true, |
|
|
[COLUMN_KEYS.OPERATE]: true, |
|
|
}; |
|
|
}; |
|
|
|
|
|
const initDefaultColumns = () => { |
|
|
const defaults = getDefaultColumnVisibility(); |
|
|
setVisibleColumns(defaults); |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const savedColumns = localStorage.getItem('channels-table-columns'); |
|
|
if (savedColumns) { |
|
|
try { |
|
|
const parsed = JSON.parse(savedColumns); |
|
|
const defaults = getDefaultColumnVisibility(); |
|
|
const merged = { ...defaults, ...parsed }; |
|
|
setVisibleColumns(merged); |
|
|
} catch (e) { |
|
|
console.error('Failed to parse saved column preferences', e); |
|
|
initDefaultColumns(); |
|
|
} |
|
|
} else { |
|
|
initDefaultColumns(); |
|
|
} |
|
|
}, []); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (Object.keys(visibleColumns).length > 0) { |
|
|
localStorage.setItem( |
|
|
'channels-table-columns', |
|
|
JSON.stringify(visibleColumns), |
|
|
); |
|
|
} |
|
|
}, [visibleColumns]); |
|
|
|
|
|
const handleColumnVisibilityChange = (columnKey, checked) => { |
|
|
const updatedColumns = { ...visibleColumns, [columnKey]: checked }; |
|
|
setVisibleColumns(updatedColumns); |
|
|
}; |
|
|
|
|
|
const handleSelectAll = (checked) => { |
|
|
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); |
|
|
const updatedColumns = {}; |
|
|
allKeys.forEach((key) => { |
|
|
updatedColumns[key] = checked; |
|
|
}); |
|
|
setVisibleColumns(updatedColumns); |
|
|
}; |
|
|
|
|
|
|
|
|
const setChannelFormat = (channels, enableTagMode) => { |
|
|
let channelDates = []; |
|
|
let channelTags = {}; |
|
|
|
|
|
for (let i = 0; i < channels.length; i++) { |
|
|
channels[i].key = '' + channels[i].id; |
|
|
if (!enableTagMode) { |
|
|
channelDates.push(channels[i]); |
|
|
} else { |
|
|
let tag = channels[i].tag ? channels[i].tag : ''; |
|
|
let tagIndex = channelTags[tag]; |
|
|
let tagChannelDates = undefined; |
|
|
|
|
|
if (tagIndex === undefined) { |
|
|
channelTags[tag] = 1; |
|
|
tagChannelDates = { |
|
|
key: tag, |
|
|
id: tag, |
|
|
tag: tag, |
|
|
name: '标签:' + tag, |
|
|
group: '', |
|
|
used_quota: 0, |
|
|
response_time: 0, |
|
|
priority: -1, |
|
|
weight: -1, |
|
|
}; |
|
|
tagChannelDates.children = []; |
|
|
channelDates.push(tagChannelDates); |
|
|
} else { |
|
|
tagChannelDates = channelDates.find((item) => item.key === tag); |
|
|
} |
|
|
|
|
|
if (tagChannelDates.priority === -1) { |
|
|
tagChannelDates.priority = channels[i].priority; |
|
|
} else { |
|
|
if (tagChannelDates.priority !== channels[i].priority) { |
|
|
tagChannelDates.priority = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
if (tagChannelDates.weight === -1) { |
|
|
tagChannelDates.weight = channels[i].weight; |
|
|
} else { |
|
|
if (tagChannelDates.weight !== channels[i].weight) { |
|
|
tagChannelDates.weight = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
if (tagChannelDates.group === '') { |
|
|
tagChannelDates.group = channels[i].group; |
|
|
} else { |
|
|
let channelGroupsStr = channels[i].group; |
|
|
channelGroupsStr.split(',').forEach((item, index) => { |
|
|
if (tagChannelDates.group.indexOf(item) === -1) { |
|
|
tagChannelDates.group += ',' + item; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
tagChannelDates.children.push(channels[i]); |
|
|
if (channels[i].status === 1) { |
|
|
tagChannelDates.status = 1; |
|
|
} |
|
|
tagChannelDates.used_quota += channels[i].used_quota; |
|
|
tagChannelDates.response_time += channels[i].response_time; |
|
|
tagChannelDates.response_time = tagChannelDates.response_time / 2; |
|
|
} |
|
|
} |
|
|
setChannels(channelDates); |
|
|
}; |
|
|
|
|
|
|
|
|
const getFormValues = () => { |
|
|
const formValues = formApi ? formApi.getValues() : {}; |
|
|
return { |
|
|
searchKeyword: formValues.searchKeyword || '', |
|
|
searchGroup: formValues.searchGroup || '', |
|
|
searchModel: formValues.searchModel || '', |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const loadChannels = async ( |
|
|
page, |
|
|
pageSize, |
|
|
idSort, |
|
|
enableTagMode, |
|
|
typeKey = activeTypeKey, |
|
|
statusF, |
|
|
) => { |
|
|
if (statusF === undefined) statusF = statusFilter; |
|
|
|
|
|
const { searchKeyword, searchGroup, searchModel } = getFormValues(); |
|
|
if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') { |
|
|
setLoading(true); |
|
|
await searchChannels( |
|
|
enableTagMode, |
|
|
typeKey, |
|
|
statusF, |
|
|
page, |
|
|
pageSize, |
|
|
idSort, |
|
|
); |
|
|
setLoading(false); |
|
|
return; |
|
|
} |
|
|
|
|
|
const reqId = ++requestCounter.current; |
|
|
setLoading(true); |
|
|
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; |
|
|
const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; |
|
|
const res = await API.get( |
|
|
`/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`, |
|
|
); |
|
|
|
|
|
if (res === undefined || reqId !== requestCounter.current) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
const { items, total, type_counts } = data; |
|
|
if (type_counts) { |
|
|
const sumAll = Object.values(type_counts).reduce( |
|
|
(acc, v) => acc + v, |
|
|
0, |
|
|
); |
|
|
setTypeCounts({ ...type_counts, all: sumAll }); |
|
|
} |
|
|
setChannelFormat(items, enableTagMode); |
|
|
setChannelCount(total); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
setLoading(false); |
|
|
}; |
|
|
|
|
|
|
|
|
const searchChannels = async ( |
|
|
enableTagMode, |
|
|
typeKey = activeTypeKey, |
|
|
statusF = statusFilter, |
|
|
page = 1, |
|
|
pageSz = pageSize, |
|
|
sortFlag = idSort, |
|
|
) => { |
|
|
const { searchKeyword, searchGroup, searchModel } = getFormValues(); |
|
|
setSearching(true); |
|
|
try { |
|
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') { |
|
|
await loadChannels( |
|
|
page, |
|
|
pageSz, |
|
|
sortFlag, |
|
|
enableTagMode, |
|
|
typeKey, |
|
|
statusF, |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; |
|
|
const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; |
|
|
const res = await API.get( |
|
|
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`, |
|
|
); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
const { items = [], total = 0, type_counts = {} } = data; |
|
|
const sumAll = Object.values(type_counts).reduce( |
|
|
(acc, v) => acc + v, |
|
|
0, |
|
|
); |
|
|
setTypeCounts({ ...type_counts, all: sumAll }); |
|
|
setChannelFormat(items, enableTagMode); |
|
|
setChannelCount(total); |
|
|
setActivePage(page); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
} finally { |
|
|
setSearching(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const refresh = async (page = activePage) => { |
|
|
const { searchKeyword, searchGroup, searchModel } = getFormValues(); |
|
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') { |
|
|
await loadChannels(page, pageSize, idSort, enableTagMode); |
|
|
} else { |
|
|
await searchChannels( |
|
|
enableTagMode, |
|
|
activeTypeKey, |
|
|
statusFilter, |
|
|
page, |
|
|
pageSize, |
|
|
idSort, |
|
|
); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const manageChannel = async (id, action, record, value) => { |
|
|
let data = { id }; |
|
|
let res; |
|
|
switch (action) { |
|
|
case 'delete': |
|
|
res = await API.delete(`/api/channel/${id}/`); |
|
|
break; |
|
|
case 'enable': |
|
|
data.status = 1; |
|
|
res = await API.put('/api/channel/', data); |
|
|
break; |
|
|
case 'disable': |
|
|
data.status = 2; |
|
|
res = await API.put('/api/channel/', data); |
|
|
break; |
|
|
case 'priority': |
|
|
if (value === '') return; |
|
|
data.priority = parseInt(value); |
|
|
res = await API.put('/api/channel/', data); |
|
|
break; |
|
|
case 'weight': |
|
|
if (value === '') return; |
|
|
data.weight = parseInt(value); |
|
|
if (data.weight < 0) data.weight = 0; |
|
|
res = await API.put('/api/channel/', data); |
|
|
break; |
|
|
case 'enable_all': |
|
|
data.channel_info = record.channel_info; |
|
|
data.channel_info.multi_key_status_list = {}; |
|
|
res = await API.put('/api/channel/', data); |
|
|
break; |
|
|
} |
|
|
const { success, message } = res.data; |
|
|
if (success) { |
|
|
showSuccess(t('操作成功完成!')); |
|
|
let channel = res.data.data; |
|
|
let newChannels = [...channels]; |
|
|
if (action !== 'delete') { |
|
|
record.status = channel.status; |
|
|
} |
|
|
setChannels(newChannels); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const manageTag = async (tag, action) => { |
|
|
let res; |
|
|
switch (action) { |
|
|
case 'enable': |
|
|
res = await API.post('/api/channel/tag/enabled', { tag: tag }); |
|
|
break; |
|
|
case 'disable': |
|
|
res = await API.post('/api/channel/tag/disabled', { tag: tag }); |
|
|
break; |
|
|
} |
|
|
const { success, message } = res.data; |
|
|
if (success) { |
|
|
showSuccess('操作成功完成!'); |
|
|
let newChannels = [...channels]; |
|
|
for (let i = 0; i < newChannels.length; i++) { |
|
|
if (newChannels[i].tag === tag) { |
|
|
let status = action === 'enable' ? 1 : 2; |
|
|
newChannels[i]?.children?.forEach((channel) => { |
|
|
channel.status = status; |
|
|
}); |
|
|
newChannels[i].status = status; |
|
|
} |
|
|
} |
|
|
setChannels(newChannels); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handlePageChange = (page) => { |
|
|
const { searchKeyword, searchGroup, searchModel } = getFormValues(); |
|
|
setActivePage(page); |
|
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') { |
|
|
loadChannels(page, pageSize, idSort, enableTagMode).then(() => {}); |
|
|
} else { |
|
|
searchChannels( |
|
|
enableTagMode, |
|
|
activeTypeKey, |
|
|
statusFilter, |
|
|
page, |
|
|
pageSize, |
|
|
idSort, |
|
|
); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handlePageSizeChange = async (size) => { |
|
|
localStorage.setItem('page-size', size + ''); |
|
|
setPageSize(size); |
|
|
setActivePage(1); |
|
|
const { searchKeyword, searchGroup, searchModel } = getFormValues(); |
|
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') { |
|
|
loadChannels(1, size, idSort, enableTagMode) |
|
|
.then() |
|
|
.catch((reason) => { |
|
|
showError(reason); |
|
|
}); |
|
|
} else { |
|
|
searchChannels( |
|
|
enableTagMode, |
|
|
activeTypeKey, |
|
|
statusFilter, |
|
|
1, |
|
|
size, |
|
|
idSort, |
|
|
); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const fetchGroups = async () => { |
|
|
try { |
|
|
let res = await API.get(`/api/group/`); |
|
|
if (res === undefined) return; |
|
|
setGroupOptions( |
|
|
res.data.data.map((group) => ({ |
|
|
label: group, |
|
|
value: group, |
|
|
})), |
|
|
); |
|
|
} catch (error) { |
|
|
showError(error.message); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const copySelectedChannel = async (record) => { |
|
|
try { |
|
|
const res = await API.post(`/api/channel/copy/${record.id}`); |
|
|
if (res?.data?.success) { |
|
|
showSuccess(t('渠道复制成功')); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(res?.data?.message || t('渠道复制失败')); |
|
|
} |
|
|
} catch (error) { |
|
|
showError( |
|
|
t('渠道复制失败: ') + |
|
|
(error?.response?.data?.message || error?.message || error), |
|
|
); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const updateChannelProperty = (channelId, updateFn) => { |
|
|
const newChannels = [...channels]; |
|
|
let updated = false; |
|
|
|
|
|
newChannels.forEach((channel) => { |
|
|
if (channel.children !== undefined) { |
|
|
channel.children.forEach((child) => { |
|
|
if (child.id === channelId) { |
|
|
updateFn(child); |
|
|
updated = true; |
|
|
} |
|
|
}); |
|
|
} else if (channel.id === channelId) { |
|
|
updateFn(channel); |
|
|
updated = true; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (updated) { |
|
|
setChannels(newChannels); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const submitTagEdit = async (type, data) => { |
|
|
switch (type) { |
|
|
case 'priority': |
|
|
if (data.priority === undefined || data.priority === '') { |
|
|
showInfo('优先级必须是整数!'); |
|
|
return; |
|
|
} |
|
|
data.priority = parseInt(data.priority); |
|
|
break; |
|
|
case 'weight': |
|
|
if ( |
|
|
data.weight === undefined || |
|
|
data.weight < 0 || |
|
|
data.weight === '' |
|
|
) { |
|
|
showInfo('权重必须是非负整数!'); |
|
|
return; |
|
|
} |
|
|
data.weight = parseInt(data.weight); |
|
|
break; |
|
|
} |
|
|
|
|
|
try { |
|
|
const res = await API.put('/api/channel/tag', data); |
|
|
if (res?.data?.success) { |
|
|
showSuccess('更新成功!'); |
|
|
await refresh(); |
|
|
} |
|
|
} catch (error) { |
|
|
showError(error); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const closeEdit = () => { |
|
|
setShowEdit(false); |
|
|
}; |
|
|
|
|
|
|
|
|
const handleRow = (record, index) => { |
|
|
if (record.status !== 1) { |
|
|
return { |
|
|
style: { |
|
|
background: 'var(--semi-color-disabled-border)', |
|
|
}, |
|
|
}; |
|
|
} else { |
|
|
return {}; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const batchSetChannelTag = async () => { |
|
|
if (selectedChannels.length === 0) { |
|
|
showError(t('请先选择要设置标签的渠道!')); |
|
|
return; |
|
|
} |
|
|
if (batchSetTagValue === '') { |
|
|
showError(t('标签不能为空!')); |
|
|
return; |
|
|
} |
|
|
let ids = selectedChannels.map((channel) => channel.id); |
|
|
const res = await API.post('/api/channel/batch/tag', { |
|
|
ids: ids, |
|
|
tag: batchSetTagValue === '' ? null : batchSetTagValue, |
|
|
}); |
|
|
if (res.data.success) { |
|
|
showSuccess( |
|
|
t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data), |
|
|
); |
|
|
await refresh(); |
|
|
setShowBatchSetTag(false); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const batchDeleteChannels = async () => { |
|
|
if (selectedChannels.length === 0) { |
|
|
showError(t('请先选择要删除的通道!')); |
|
|
return; |
|
|
} |
|
|
setLoading(true); |
|
|
let ids = []; |
|
|
selectedChannels.forEach((channel) => { |
|
|
ids.push(channel.id); |
|
|
}); |
|
|
const res = await API.post(`/api/channel/batch`, { ids: ids }); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data)); |
|
|
await refresh(); |
|
|
setTimeout(() => { |
|
|
if (channels.length === 0 && activePage > 1) { |
|
|
refresh(activePage - 1); |
|
|
} |
|
|
}, 100); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
setLoading(false); |
|
|
}; |
|
|
|
|
|
|
|
|
const testAllChannels = async () => { |
|
|
const res = await API.get(`/api/channel/test`); |
|
|
const { success, message } = res.data; |
|
|
if (success) { |
|
|
showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。')); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const deleteAllDisabledChannels = async () => { |
|
|
const res = await API.delete(`/api/channel/disabled`); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
showSuccess( |
|
|
t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data), |
|
|
); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const updateAllChannelsBalance = async () => { |
|
|
const res = await API.get(`/api/channel/update_balance`); |
|
|
const { success, message } = res.data; |
|
|
if (success) { |
|
|
showInfo(t('已更新完毕所有已启用通道余额!')); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const updateChannelBalance = async (record) => { |
|
|
const res = await API.get(`/api/channel/update_balance/${record.id}/`); |
|
|
const { success, message, balance } = res.data; |
|
|
if (success) { |
|
|
updateChannelProperty(record.id, (channel) => { |
|
|
channel.balance = balance; |
|
|
channel.balance_updated_time = Date.now() / 1000; |
|
|
}); |
|
|
showInfo( |
|
|
t('通道 ${name} 余额更新成功!').replace('${name}', record.name), |
|
|
); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const fixChannelsAbilities = async () => { |
|
|
const res = await API.post(`/api/channel/fix`); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
showSuccess( |
|
|
t('已修复 ${success} 个通道,失败 ${fails} 个通道。') |
|
|
.replace('${success}', data.success) |
|
|
.replace('${fails}', data.fails), |
|
|
); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const testChannel = async (record, model, endpointType = '') => { |
|
|
const testKey = `${record.id}-${model}`; |
|
|
|
|
|
|
|
|
if (shouldStopBatchTestingRef.current && isBatchTesting) { |
|
|
return Promise.resolve(); |
|
|
} |
|
|
|
|
|
|
|
|
setTestingModels((prev) => new Set([...prev, model])); |
|
|
|
|
|
try { |
|
|
let url = `/api/channel/test/${record.id}?model=${model}`; |
|
|
if (endpointType) { |
|
|
url += `&endpoint_type=${endpointType}`; |
|
|
} |
|
|
const res = await API.get(url); |
|
|
|
|
|
|
|
|
if (shouldStopBatchTestingRef.current && isBatchTesting) { |
|
|
return Promise.resolve(); |
|
|
} |
|
|
|
|
|
const { success, message, time } = res.data; |
|
|
|
|
|
|
|
|
setModelTestResults((prev) => ({ |
|
|
...prev, |
|
|
[testKey]: { |
|
|
success, |
|
|
message, |
|
|
time: time || 0, |
|
|
timestamp: Date.now(), |
|
|
}, |
|
|
})); |
|
|
|
|
|
if (success) { |
|
|
|
|
|
updateChannelProperty(record.id, (channel) => { |
|
|
channel.response_time = time * 1000; |
|
|
channel.test_time = Date.now() / 1000; |
|
|
}); |
|
|
|
|
|
if (!model || model === '') { |
|
|
showInfo( |
|
|
t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。') |
|
|
.replace('${name}', record.name) |
|
|
.replace('${time.toFixed(2)}', time.toFixed(2)), |
|
|
); |
|
|
} else { |
|
|
showInfo( |
|
|
t( |
|
|
'通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。', |
|
|
) |
|
|
.replace('${name}', record.name) |
|
|
.replace('${model}', model) |
|
|
.replace('${time.toFixed(2)}', time.toFixed(2)), |
|
|
); |
|
|
} |
|
|
} else { |
|
|
showError(`${t('模型')} ${model}: ${message}`); |
|
|
} |
|
|
} catch (error) { |
|
|
|
|
|
const testKey = `${record.id}-${model}`; |
|
|
setModelTestResults((prev) => ({ |
|
|
...prev, |
|
|
[testKey]: { |
|
|
success: false, |
|
|
message: error.message || t('网络错误'), |
|
|
time: 0, |
|
|
timestamp: Date.now(), |
|
|
}, |
|
|
})); |
|
|
showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`); |
|
|
} finally { |
|
|
|
|
|
setTestingModels((prev) => { |
|
|
const newSet = new Set(prev); |
|
|
newSet.delete(model); |
|
|
return newSet; |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const batchTestModels = async () => { |
|
|
if (!currentTestChannel || !currentTestChannel.models) { |
|
|
showError(t('渠道模型信息不完整')); |
|
|
return; |
|
|
} |
|
|
|
|
|
const models = currentTestChannel.models |
|
|
.split(',') |
|
|
.filter((model) => |
|
|
model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), |
|
|
); |
|
|
|
|
|
if (models.length === 0) { |
|
|
showError(t('没有找到匹配的模型')); |
|
|
return; |
|
|
} |
|
|
|
|
|
setIsBatchTesting(true); |
|
|
shouldStopBatchTestingRef.current = false; |
|
|
|
|
|
|
|
|
setModelTestResults((prev) => { |
|
|
const newResults = { ...prev }; |
|
|
models.forEach((model) => { |
|
|
const testKey = `${currentTestChannel.id}-${model}`; |
|
|
delete newResults[testKey]; |
|
|
}); |
|
|
return newResults; |
|
|
}); |
|
|
|
|
|
try { |
|
|
showInfo( |
|
|
t('开始批量测试 ${count} 个模型,已清空上次结果...').replace( |
|
|
'${count}', |
|
|
models.length, |
|
|
), |
|
|
); |
|
|
|
|
|
|
|
|
const concurrencyLimit = 5; |
|
|
const results = []; |
|
|
|
|
|
for (let i = 0; i < models.length; i += concurrencyLimit) { |
|
|
|
|
|
if (shouldStopBatchTestingRef.current) { |
|
|
showInfo(t('批量测试已停止')); |
|
|
break; |
|
|
} |
|
|
|
|
|
const batch = models.slice(i, i + concurrencyLimit); |
|
|
showInfo( |
|
|
t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)') |
|
|
.replace('${current}', i + 1) |
|
|
.replace('${end}', Math.min(i + concurrencyLimit, models.length)) |
|
|
.replace('${total}', models.length), |
|
|
); |
|
|
|
|
|
const batchPromises = batch.map((model) => |
|
|
testChannel(currentTestChannel, model, selectedEndpointType), |
|
|
); |
|
|
const batchResults = await Promise.allSettled(batchPromises); |
|
|
results.push(...batchResults); |
|
|
|
|
|
|
|
|
if (shouldStopBatchTestingRef.current) { |
|
|
showInfo(t('批量测试已停止')); |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
if (i + concurrencyLimit < models.length) { |
|
|
await new Promise((resolve) => setTimeout(resolve, 100)); |
|
|
} |
|
|
} |
|
|
|
|
|
if (!shouldStopBatchTestingRef.current) { |
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300)); |
|
|
|
|
|
|
|
|
setModelTestResults((currentResults) => { |
|
|
let successCount = 0; |
|
|
let failCount = 0; |
|
|
|
|
|
models.forEach((model) => { |
|
|
const testKey = `${currentTestChannel.id}-${model}`; |
|
|
const result = currentResults[testKey]; |
|
|
if (result && result.success) { |
|
|
successCount++; |
|
|
} else { |
|
|
failCount++; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
showSuccess( |
|
|
t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') |
|
|
.replace('${success}', successCount) |
|
|
.replace('${fail}', failCount) |
|
|
.replace('${total}', models.length), |
|
|
); |
|
|
}, 100); |
|
|
|
|
|
return currentResults; |
|
|
}); |
|
|
} |
|
|
} catch (error) { |
|
|
showError(t('批量测试过程中发生错误: ') + error.message); |
|
|
} finally { |
|
|
setIsBatchTesting(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const stopBatchTesting = () => { |
|
|
shouldStopBatchTestingRef.current = true; |
|
|
setIsBatchTesting(false); |
|
|
setTestingModels(new Set()); |
|
|
showInfo(t('已停止批量测试')); |
|
|
}; |
|
|
|
|
|
|
|
|
const clearTestResults = () => { |
|
|
setModelTestResults({}); |
|
|
showInfo(t('已清空测试结果')); |
|
|
}; |
|
|
|
|
|
|
|
|
const handleCloseModal = () => { |
|
|
|
|
|
if (isBatchTesting) { |
|
|
shouldStopBatchTestingRef.current = true; |
|
|
showInfo(t('关闭弹窗,已停止批量测试')); |
|
|
} |
|
|
|
|
|
setShowModelTestModal(false); |
|
|
setModelSearchKeyword(''); |
|
|
setIsBatchTesting(false); |
|
|
setTestingModels(new Set()); |
|
|
setSelectedModelKeys([]); |
|
|
setModelTablePage(1); |
|
|
setSelectedEndpointType(''); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const channelTypeCounts = useMemo(() => { |
|
|
if (Object.keys(typeCounts).length > 0) return typeCounts; |
|
|
const counts = { all: channels.length }; |
|
|
channels.forEach((channel) => { |
|
|
const collect = (ch) => { |
|
|
const type = ch.type; |
|
|
counts[type] = (counts[type] || 0) + 1; |
|
|
}; |
|
|
if (channel.children !== undefined) { |
|
|
channel.children.forEach(collect); |
|
|
} else { |
|
|
collect(channel); |
|
|
} |
|
|
}); |
|
|
return counts; |
|
|
}, [typeCounts, channels]); |
|
|
|
|
|
const availableTypeKeys = useMemo(() => { |
|
|
const keys = ['all']; |
|
|
Object.entries(channelTypeCounts).forEach(([k, v]) => { |
|
|
if (k !== 'all' && v > 0) keys.push(String(k)); |
|
|
}); |
|
|
return keys; |
|
|
}, [channelTypeCounts]); |
|
|
|
|
|
return { |
|
|
|
|
|
channels, |
|
|
loading, |
|
|
searching, |
|
|
activePage, |
|
|
pageSize, |
|
|
channelCount, |
|
|
groupOptions, |
|
|
idSort, |
|
|
enableTagMode, |
|
|
enableBatchDelete, |
|
|
statusFilter, |
|
|
compactMode, |
|
|
|
|
|
|
|
|
showEdit, |
|
|
setShowEdit, |
|
|
editingChannel, |
|
|
setEditingChannel, |
|
|
showEditTag, |
|
|
setShowEditTag, |
|
|
editingTag, |
|
|
setEditingTag, |
|
|
selectedChannels, |
|
|
setSelectedChannels, |
|
|
showBatchSetTag, |
|
|
setShowBatchSetTag, |
|
|
batchSetTagValue, |
|
|
setBatchSetTagValue, |
|
|
|
|
|
|
|
|
visibleColumns, |
|
|
showColumnSelector, |
|
|
setShowColumnSelector, |
|
|
COLUMN_KEYS, |
|
|
|
|
|
|
|
|
activeTypeKey, |
|
|
setActiveTypeKey, |
|
|
typeCounts, |
|
|
channelTypeCounts, |
|
|
availableTypeKeys, |
|
|
|
|
|
|
|
|
showModelTestModal, |
|
|
setShowModelTestModal, |
|
|
currentTestChannel, |
|
|
setCurrentTestChannel, |
|
|
modelSearchKeyword, |
|
|
setModelSearchKeyword, |
|
|
modelTestResults, |
|
|
testingModels, |
|
|
selectedModelKeys, |
|
|
setSelectedModelKeys, |
|
|
isBatchTesting, |
|
|
modelTablePage, |
|
|
setModelTablePage, |
|
|
selectedEndpointType, |
|
|
setSelectedEndpointType, |
|
|
allSelectingRef, |
|
|
|
|
|
|
|
|
showMultiKeyManageModal, |
|
|
setShowMultiKeyManageModal, |
|
|
currentMultiKeyChannel, |
|
|
setCurrentMultiKeyChannel, |
|
|
|
|
|
|
|
|
formApi, |
|
|
setFormApi, |
|
|
formInitValues, |
|
|
|
|
|
|
|
|
t, |
|
|
isMobile, |
|
|
|
|
|
|
|
|
loadChannels, |
|
|
searchChannels, |
|
|
refresh, |
|
|
manageChannel, |
|
|
manageTag, |
|
|
handlePageChange, |
|
|
handlePageSizeChange, |
|
|
copySelectedChannel, |
|
|
updateChannelProperty, |
|
|
submitTagEdit, |
|
|
closeEdit, |
|
|
handleRow, |
|
|
batchSetChannelTag, |
|
|
batchDeleteChannels, |
|
|
testAllChannels, |
|
|
deleteAllDisabledChannels, |
|
|
updateAllChannelsBalance, |
|
|
updateChannelBalance, |
|
|
fixChannelsAbilities, |
|
|
testChannel, |
|
|
batchTestModels, |
|
|
handleCloseModal, |
|
|
getFormValues, |
|
|
|
|
|
|
|
|
handleColumnVisibilityChange, |
|
|
handleSelectAll, |
|
|
initDefaultColumns, |
|
|
getDefaultColumnVisibility, |
|
|
|
|
|
|
|
|
setIdSort, |
|
|
setEnableTagMode, |
|
|
setEnableBatchDelete, |
|
|
setStatusFilter, |
|
|
setCompactMode, |
|
|
setActivePage, |
|
|
}; |
|
|
}; |
|
|
|