/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ 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(); // Basic states 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([]); // UI states 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'); // Column visibility states const [visibleColumns, setVisibleColumns] = useState({}); const [showColumnSelector, setShowColumnSelector] = useState(false); // Status filter const [statusFilter, setStatusFilter] = useState( localStorage.getItem('channel-status-filter') || 'all', ); // Type tabs states const [activeTypeKey, setActiveTypeKey] = useState('all'); const [typeCounts, setTypeCounts] = useState({}); // Model test states 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(''); // 使用 ref 来避免闭包问题,类似旧版实现 const shouldStopBatchTestingRef = useRef(false); // Multi-key management states const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false); const [currentMultiKeyChannel, setCurrentMultiKeyChannel] = useState(null); // Refs const requestCounter = useRef(0); const allSelectingRef = useRef(false); const [formApi, setFormApi] = useState(null); const formInitValues = { searchKeyword: '', searchGroup: '', searchModel: '', }; // Column keys 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', }; // Initialize from localStorage 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(); }, []); // Column visibility management 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); }; // Load saved column preferences 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(); } }, []); // Save column preferences 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); }; // Data formatting 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); }; // Get form values helper const getFormValues = () => { const formValues = formApi ? formApi.getValues() : {}; return { searchKeyword: formValues.searchKeyword || '', searchGroup: formValues.searchGroup || '', searchModel: formValues.searchModel || '', }; }; // Load channels 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); }; // Search channels 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); } }; // Refresh 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, ); } }; // Channel management 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); } }; // Tag management 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); } }; // Page handlers 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, ); } }; // Fetch groups 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); } }; // Copy channel 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), ); } }; // Update channel property 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); } }; // Tag edit 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); } }; // Close edit const closeEdit = () => { setShowEdit(false); }; // Row style const handleRow = (record, index) => { if (record.status !== 1) { return { style: { background: 'var(--semi-color-disabled-border)', }, }; } else { return {}; } }; // Batch operations 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); }; // Channel operations 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); } }; // Test channel - 单个模型测试,参考旧版实现 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('已清空测试结果')); }; // Handle close modal const handleCloseModal = () => { // 如果正在批量测试,先停止测试 if (isBatchTesting) { shouldStopBatchTestingRef.current = true; showInfo(t('关闭弹窗,已停止批量测试')); } setShowModelTestModal(false); setModelSearchKeyword(''); setIsBatchTesting(false); setTestingModels(new Set()); setSelectedModelKeys([]); setModelTablePage(1); setSelectedEndpointType(''); // 可选择性保留测试结果,这里不清空以便用户查看 }; // Type counts 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 { // Basic states channels, loading, searching, activePage, pageSize, channelCount, groupOptions, idSort, enableTagMode, enableBatchDelete, statusFilter, compactMode, // UI states showEdit, setShowEdit, editingChannel, setEditingChannel, showEditTag, setShowEditTag, editingTag, setEditingTag, selectedChannels, setSelectedChannels, showBatchSetTag, setShowBatchSetTag, batchSetTagValue, setBatchSetTagValue, // Column states visibleColumns, showColumnSelector, setShowColumnSelector, COLUMN_KEYS, // Type tab states activeTypeKey, setActiveTypeKey, typeCounts, channelTypeCounts, availableTypeKeys, // Model test states showModelTestModal, setShowModelTestModal, currentTestChannel, setCurrentTestChannel, modelSearchKeyword, setModelSearchKeyword, modelTestResults, testingModels, selectedModelKeys, setSelectedModelKeys, isBatchTesting, modelTablePage, setModelTablePage, selectedEndpointType, setSelectedEndpointType, allSelectingRef, // Multi-key management states showMultiKeyManageModal, setShowMultiKeyManageModal, currentMultiKeyChannel, setCurrentMultiKeyChannel, // Form formApi, setFormApi, formInitValues, // Helpers t, isMobile, // Functions loadChannels, searchChannels, refresh, manageChannel, manageTag, handlePageChange, handlePageSizeChange, copySelectedChannel, updateChannelProperty, submitTagEdit, closeEdit, handleRow, batchSetChannelTag, batchDeleteChannels, testAllChannels, deleteAllDisabledChannels, updateAllChannelsBalance, updateChannelBalance, fixChannelsAbilities, testChannel, batchTestModels, handleCloseModal, getFormValues, // Column functions handleColumnVisibilityChange, handleSelectAll, initDefaultColumns, getDefaultColumnVisibility, // Setters setIdSort, setEnableTagMode, setEnableBatchDelete, setStatusFilter, setCompactMode, setActivePage, }; };