useChannelsData.jsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useState, useEffect, useRef, useMemo } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import {
  18. API,
  19. showError,
  20. showInfo,
  21. showSuccess,
  22. loadChannelModels,
  23. copy
  24. } from '../../helpers';
  25. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants';
  26. import { useIsMobile } from '../common/useIsMobile';
  27. import { useTableCompactMode } from '../common/useTableCompactMode';
  28. import { Modal } from '@douyinfe/semi-ui';
  29. export const useChannelsData = () => {
  30. const { t } = useTranslation();
  31. const isMobile = useIsMobile();
  32. // Basic states
  33. const [channels, setChannels] = useState([]);
  34. const [loading, setLoading] = useState(true);
  35. const [activePage, setActivePage] = useState(1);
  36. const [idSort, setIdSort] = useState(false);
  37. const [searching, setSearching] = useState(false);
  38. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  39. const [channelCount, setChannelCount] = useState(0);
  40. const [groupOptions, setGroupOptions] = useState([]);
  41. // UI states
  42. const [showEdit, setShowEdit] = useState(false);
  43. const [enableBatchDelete, setEnableBatchDelete] = useState(false);
  44. const [editingChannel, setEditingChannel] = useState({ id: undefined });
  45. const [showEditTag, setShowEditTag] = useState(false);
  46. const [editingTag, setEditingTag] = useState('');
  47. const [selectedChannels, setSelectedChannels] = useState([]);
  48. const [enableTagMode, setEnableTagMode] = useState(false);
  49. const [showBatchSetTag, setShowBatchSetTag] = useState(false);
  50. const [batchSetTagValue, setBatchSetTagValue] = useState('');
  51. const [compactMode, setCompactMode] = useTableCompactMode('channels');
  52. // Column visibility states
  53. const [visibleColumns, setVisibleColumns] = useState({});
  54. const [showColumnSelector, setShowColumnSelector] = useState(false);
  55. // Status filter
  56. const [statusFilter, setStatusFilter] = useState(
  57. localStorage.getItem('channel-status-filter') || 'all'
  58. );
  59. // Type tabs states
  60. const [activeTypeKey, setActiveTypeKey] = useState('all');
  61. const [typeCounts, setTypeCounts] = useState({});
  62. // Model test states
  63. const [showModelTestModal, setShowModelTestModal] = useState(false);
  64. const [currentTestChannel, setCurrentTestChannel] = useState(null);
  65. const [modelSearchKeyword, setModelSearchKeyword] = useState('');
  66. const [modelTestResults, setModelTestResults] = useState({});
  67. const [testingModels, setTestingModels] = useState(new Set());
  68. const [selectedModelKeys, setSelectedModelKeys] = useState([]);
  69. const [isBatchTesting, setIsBatchTesting] = useState(false);
  70. const [modelTablePage, setModelTablePage] = useState(1);
  71. // 使用 ref 来避免闭包问题,类似旧版实现
  72. const shouldStopBatchTestingRef = useRef(false);
  73. // Multi-key management states
  74. const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false);
  75. const [currentMultiKeyChannel, setCurrentMultiKeyChannel] = useState(null);
  76. // Refs
  77. const requestCounter = useRef(0);
  78. const allSelectingRef = useRef(false);
  79. const [formApi, setFormApi] = useState(null);
  80. const formInitValues = {
  81. searchKeyword: '',
  82. searchGroup: '',
  83. searchModel: '',
  84. };
  85. // Column keys
  86. const COLUMN_KEYS = {
  87. ID: 'id',
  88. NAME: 'name',
  89. GROUP: 'group',
  90. TYPE: 'type',
  91. STATUS: 'status',
  92. RESPONSE_TIME: 'response_time',
  93. BALANCE: 'balance',
  94. PRIORITY: 'priority',
  95. WEIGHT: 'weight',
  96. OPERATE: 'operate',
  97. };
  98. // Initialize from localStorage
  99. useEffect(() => {
  100. const localIdSort = localStorage.getItem('id-sort') === 'true';
  101. const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  102. const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true';
  103. const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true';
  104. setIdSort(localIdSort);
  105. setPageSize(localPageSize);
  106. setEnableTagMode(localEnableTagMode);
  107. setEnableBatchDelete(localEnableBatchDelete);
  108. loadChannels(1, localPageSize, localIdSort, localEnableTagMode)
  109. .then()
  110. .catch((reason) => {
  111. showError(reason);
  112. });
  113. fetchGroups().then();
  114. loadChannelModels().then();
  115. }, []);
  116. // Column visibility management
  117. const getDefaultColumnVisibility = () => {
  118. return {
  119. [COLUMN_KEYS.ID]: true,
  120. [COLUMN_KEYS.NAME]: true,
  121. [COLUMN_KEYS.GROUP]: true,
  122. [COLUMN_KEYS.TYPE]: true,
  123. [COLUMN_KEYS.STATUS]: true,
  124. [COLUMN_KEYS.RESPONSE_TIME]: true,
  125. [COLUMN_KEYS.BALANCE]: true,
  126. [COLUMN_KEYS.PRIORITY]: true,
  127. [COLUMN_KEYS.WEIGHT]: true,
  128. [COLUMN_KEYS.OPERATE]: true,
  129. };
  130. };
  131. const initDefaultColumns = () => {
  132. const defaults = getDefaultColumnVisibility();
  133. setVisibleColumns(defaults);
  134. };
  135. // Load saved column preferences
  136. useEffect(() => {
  137. const savedColumns = localStorage.getItem('channels-table-columns');
  138. if (savedColumns) {
  139. try {
  140. const parsed = JSON.parse(savedColumns);
  141. const defaults = getDefaultColumnVisibility();
  142. const merged = { ...defaults, ...parsed };
  143. setVisibleColumns(merged);
  144. } catch (e) {
  145. console.error('Failed to parse saved column preferences', e);
  146. initDefaultColumns();
  147. }
  148. } else {
  149. initDefaultColumns();
  150. }
  151. }, []);
  152. // Save column preferences
  153. useEffect(() => {
  154. if (Object.keys(visibleColumns).length > 0) {
  155. localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns));
  156. }
  157. }, [visibleColumns]);
  158. const handleColumnVisibilityChange = (columnKey, checked) => {
  159. const updatedColumns = { ...visibleColumns, [columnKey]: checked };
  160. setVisibleColumns(updatedColumns);
  161. };
  162. const handleSelectAll = (checked) => {
  163. const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
  164. const updatedColumns = {};
  165. allKeys.forEach((key) => {
  166. updatedColumns[key] = checked;
  167. });
  168. setVisibleColumns(updatedColumns);
  169. };
  170. // Data formatting
  171. const setChannelFormat = (channels, enableTagMode) => {
  172. let channelDates = [];
  173. let channelTags = {};
  174. for (let i = 0; i < channels.length; i++) {
  175. channels[i].key = '' + channels[i].id;
  176. if (!enableTagMode) {
  177. channelDates.push(channels[i]);
  178. } else {
  179. let tag = channels[i].tag ? channels[i].tag : '';
  180. let tagIndex = channelTags[tag];
  181. let tagChannelDates = undefined;
  182. if (tagIndex === undefined) {
  183. channelTags[tag] = 1;
  184. tagChannelDates = {
  185. key: tag,
  186. id: tag,
  187. tag: tag,
  188. name: '标签:' + tag,
  189. group: '',
  190. used_quota: 0,
  191. response_time: 0,
  192. priority: -1,
  193. weight: -1,
  194. };
  195. tagChannelDates.children = [];
  196. channelDates.push(tagChannelDates);
  197. } else {
  198. tagChannelDates = channelDates.find((item) => item.key === tag);
  199. }
  200. if (tagChannelDates.priority === -1) {
  201. tagChannelDates.priority = channels[i].priority;
  202. } else {
  203. if (tagChannelDates.priority !== channels[i].priority) {
  204. tagChannelDates.priority = '';
  205. }
  206. }
  207. if (tagChannelDates.weight === -1) {
  208. tagChannelDates.weight = channels[i].weight;
  209. } else {
  210. if (tagChannelDates.weight !== channels[i].weight) {
  211. tagChannelDates.weight = '';
  212. }
  213. }
  214. if (tagChannelDates.group === '') {
  215. tagChannelDates.group = channels[i].group;
  216. } else {
  217. let channelGroupsStr = channels[i].group;
  218. channelGroupsStr.split(',').forEach((item, index) => {
  219. if (tagChannelDates.group.indexOf(item) === -1) {
  220. tagChannelDates.group += ',' + item;
  221. }
  222. });
  223. }
  224. tagChannelDates.children.push(channels[i]);
  225. if (channels[i].status === 1) {
  226. tagChannelDates.status = 1;
  227. }
  228. tagChannelDates.used_quota += channels[i].used_quota;
  229. tagChannelDates.response_time += channels[i].response_time;
  230. tagChannelDates.response_time = tagChannelDates.response_time / 2;
  231. }
  232. }
  233. setChannels(channelDates);
  234. };
  235. // Get form values helper
  236. const getFormValues = () => {
  237. const formValues = formApi ? formApi.getValues() : {};
  238. return {
  239. searchKeyword: formValues.searchKeyword || '',
  240. searchGroup: formValues.searchGroup || '',
  241. searchModel: formValues.searchModel || '',
  242. };
  243. };
  244. // Load channels
  245. const loadChannels = async (
  246. page,
  247. pageSize,
  248. idSort,
  249. enableTagMode,
  250. typeKey = activeTypeKey,
  251. statusF,
  252. ) => {
  253. if (statusF === undefined) statusF = statusFilter;
  254. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  255. if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') {
  256. setLoading(true);
  257. await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort);
  258. setLoading(false);
  259. return;
  260. }
  261. const reqId = ++requestCounter.current;
  262. setLoading(true);
  263. const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
  264. const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
  265. const res = await API.get(
  266. `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
  267. );
  268. if (res === undefined || reqId !== requestCounter.current) {
  269. return;
  270. }
  271. const { success, message, data } = res.data;
  272. if (success) {
  273. const { items, total, type_counts } = data;
  274. if (type_counts) {
  275. const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
  276. setTypeCounts({ ...type_counts, all: sumAll });
  277. }
  278. setChannelFormat(items, enableTagMode);
  279. setChannelCount(total);
  280. } else {
  281. showError(message);
  282. }
  283. setLoading(false);
  284. };
  285. // Search channels
  286. const searchChannels = async (
  287. enableTagMode,
  288. typeKey = activeTypeKey,
  289. statusF = statusFilter,
  290. page = 1,
  291. pageSz = pageSize,
  292. sortFlag = idSort,
  293. ) => {
  294. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  295. setSearching(true);
  296. try {
  297. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  298. await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF);
  299. return;
  300. }
  301. const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
  302. const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
  303. const res = await API.get(
  304. `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`,
  305. );
  306. const { success, message, data } = res.data;
  307. if (success) {
  308. const { items = [], total = 0, type_counts = {} } = data;
  309. const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
  310. setTypeCounts({ ...type_counts, all: sumAll });
  311. setChannelFormat(items, enableTagMode);
  312. setChannelCount(total);
  313. setActivePage(page);
  314. } else {
  315. showError(message);
  316. }
  317. } finally {
  318. setSearching(false);
  319. }
  320. };
  321. // Refresh
  322. const refresh = async (page = activePage) => {
  323. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  324. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  325. await loadChannels(page, pageSize, idSort, enableTagMode);
  326. } else {
  327. await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
  328. }
  329. };
  330. // Channel management
  331. const manageChannel = async (id, action, record, value) => {
  332. let data = { id };
  333. let res;
  334. switch (action) {
  335. case 'delete':
  336. res = await API.delete(`/api/channel/${id}/`);
  337. break;
  338. case 'enable':
  339. data.status = 1;
  340. res = await API.put('/api/channel/', data);
  341. break;
  342. case 'disable':
  343. data.status = 2;
  344. res = await API.put('/api/channel/', data);
  345. break;
  346. case 'priority':
  347. if (value === '') return;
  348. data.priority = parseInt(value);
  349. res = await API.put('/api/channel/', data);
  350. break;
  351. case 'weight':
  352. if (value === '') return;
  353. data.weight = parseInt(value);
  354. if (data.weight < 0) data.weight = 0;
  355. res = await API.put('/api/channel/', data);
  356. break;
  357. case 'enable_all':
  358. data.channel_info = record.channel_info;
  359. data.channel_info.multi_key_status_list = {};
  360. res = await API.put('/api/channel/', data);
  361. break;
  362. }
  363. const { success, message } = res.data;
  364. if (success) {
  365. showSuccess(t('操作成功完成!'));
  366. let channel = res.data.data;
  367. let newChannels = [...channels];
  368. if (action !== 'delete') {
  369. record.status = channel.status;
  370. }
  371. setChannels(newChannels);
  372. } else {
  373. showError(message);
  374. }
  375. };
  376. // Tag management
  377. const manageTag = async (tag, action) => {
  378. let res;
  379. switch (action) {
  380. case 'enable':
  381. res = await API.post('/api/channel/tag/enabled', { tag: tag });
  382. break;
  383. case 'disable':
  384. res = await API.post('/api/channel/tag/disabled', { tag: tag });
  385. break;
  386. }
  387. const { success, message } = res.data;
  388. if (success) {
  389. showSuccess('操作成功完成!');
  390. let newChannels = [...channels];
  391. for (let i = 0; i < newChannels.length; i++) {
  392. if (newChannels[i].tag === tag) {
  393. let status = action === 'enable' ? 1 : 2;
  394. newChannels[i]?.children?.forEach((channel) => {
  395. channel.status = status;
  396. });
  397. newChannels[i].status = status;
  398. }
  399. }
  400. setChannels(newChannels);
  401. } else {
  402. showError(message);
  403. }
  404. };
  405. // Page handlers
  406. const handlePageChange = (page) => {
  407. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  408. setActivePage(page);
  409. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  410. loadChannels(page, pageSize, idSort, enableTagMode).then(() => { });
  411. } else {
  412. searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
  413. }
  414. };
  415. const handlePageSizeChange = async (size) => {
  416. localStorage.setItem('page-size', size + '');
  417. setPageSize(size);
  418. setActivePage(1);
  419. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  420. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  421. loadChannels(1, size, idSort, enableTagMode)
  422. .then()
  423. .catch((reason) => {
  424. showError(reason);
  425. });
  426. } else {
  427. searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort);
  428. }
  429. };
  430. // Fetch groups
  431. const fetchGroups = async () => {
  432. try {
  433. let res = await API.get(`/api/group/`);
  434. if (res === undefined) return;
  435. setGroupOptions(
  436. res.data.data.map((group) => ({
  437. label: group,
  438. value: group,
  439. })),
  440. );
  441. } catch (error) {
  442. showError(error.message);
  443. }
  444. };
  445. // Copy channel
  446. const copySelectedChannel = async (record) => {
  447. try {
  448. const res = await API.post(`/api/channel/copy/${record.id}`);
  449. if (res?.data?.success) {
  450. showSuccess(t('渠道复制成功'));
  451. await refresh();
  452. } else {
  453. showError(res?.data?.message || t('渠道复制失败'));
  454. }
  455. } catch (error) {
  456. showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error));
  457. }
  458. };
  459. // Update channel property
  460. const updateChannelProperty = (channelId, updateFn) => {
  461. const newChannels = [...channels];
  462. let updated = false;
  463. newChannels.forEach((channel) => {
  464. if (channel.children !== undefined) {
  465. channel.children.forEach((child) => {
  466. if (child.id === channelId) {
  467. updateFn(child);
  468. updated = true;
  469. }
  470. });
  471. } else if (channel.id === channelId) {
  472. updateFn(channel);
  473. updated = true;
  474. }
  475. });
  476. if (updated) {
  477. setChannels(newChannels);
  478. }
  479. };
  480. // Tag edit
  481. const submitTagEdit = async (type, data) => {
  482. switch (type) {
  483. case 'priority':
  484. if (data.priority === undefined || data.priority === '') {
  485. showInfo('优先级必须是整数!');
  486. return;
  487. }
  488. data.priority = parseInt(data.priority);
  489. break;
  490. case 'weight':
  491. if (data.weight === undefined || data.weight < 0 || data.weight === '') {
  492. showInfo('权重必须是非负整数!');
  493. return;
  494. }
  495. data.weight = parseInt(data.weight);
  496. break;
  497. }
  498. try {
  499. const res = await API.put('/api/channel/tag', data);
  500. if (res?.data?.success) {
  501. showSuccess('更新成功!');
  502. await refresh();
  503. }
  504. } catch (error) {
  505. showError(error);
  506. }
  507. };
  508. // Close edit
  509. const closeEdit = () => {
  510. setShowEdit(false);
  511. };
  512. // Row style
  513. const handleRow = (record, index) => {
  514. if (record.status !== 1) {
  515. return {
  516. style: {
  517. background: 'var(--semi-color-disabled-border)',
  518. },
  519. };
  520. } else {
  521. return {};
  522. }
  523. };
  524. // Batch operations
  525. const batchSetChannelTag = async () => {
  526. if (selectedChannels.length === 0) {
  527. showError(t('请先选择要设置标签的渠道!'));
  528. return;
  529. }
  530. if (batchSetTagValue === '') {
  531. showError(t('标签不能为空!'));
  532. return;
  533. }
  534. let ids = selectedChannels.map((channel) => channel.id);
  535. const res = await API.post('/api/channel/batch/tag', {
  536. ids: ids,
  537. tag: batchSetTagValue === '' ? null : batchSetTagValue,
  538. });
  539. if (res.data.success) {
  540. showSuccess(
  541. t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data),
  542. );
  543. await refresh();
  544. setShowBatchSetTag(false);
  545. } else {
  546. showError(res.data.message);
  547. }
  548. };
  549. const batchDeleteChannels = async () => {
  550. if (selectedChannels.length === 0) {
  551. showError(t('请先选择要删除的通道!'));
  552. return;
  553. }
  554. setLoading(true);
  555. let ids = [];
  556. selectedChannels.forEach((channel) => {
  557. ids.push(channel.id);
  558. });
  559. const res = await API.post(`/api/channel/batch`, { ids: ids });
  560. const { success, message, data } = res.data;
  561. if (success) {
  562. showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
  563. await refresh();
  564. setTimeout(() => {
  565. if (channels.length === 0 && activePage > 1) {
  566. refresh(activePage - 1);
  567. }
  568. }, 100);
  569. } else {
  570. showError(message);
  571. }
  572. setLoading(false);
  573. };
  574. // Channel operations
  575. const testAllChannels = async () => {
  576. const res = await API.get(`/api/channel/test`);
  577. const { success, message } = res.data;
  578. if (success) {
  579. showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
  580. } else {
  581. showError(message);
  582. }
  583. };
  584. const deleteAllDisabledChannels = async () => {
  585. const res = await API.delete(`/api/channel/disabled`);
  586. const { success, message, data } = res.data;
  587. if (success) {
  588. showSuccess(
  589. t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data),
  590. );
  591. await refresh();
  592. } else {
  593. showError(message);
  594. }
  595. };
  596. const updateAllChannelsBalance = async () => {
  597. const res = await API.get(`/api/channel/update_balance`);
  598. const { success, message } = res.data;
  599. if (success) {
  600. showInfo(t('已更新完毕所有已启用通道余额!'));
  601. } else {
  602. showError(message);
  603. }
  604. };
  605. const updateChannelBalance = async (record) => {
  606. const res = await API.get(`/api/channel/update_balance/${record.id}/`);
  607. const { success, message, balance } = res.data;
  608. if (success) {
  609. updateChannelProperty(record.id, (channel) => {
  610. channel.balance = balance;
  611. channel.balance_updated_time = Date.now() / 1000;
  612. });
  613. showInfo(
  614. t('通道 ${name} 余额更新成功!').replace('${name}', record.name),
  615. );
  616. } else {
  617. showError(message);
  618. }
  619. };
  620. const fixChannelsAbilities = async () => {
  621. const res = await API.post(`/api/channel/fix`);
  622. const { success, message, data } = res.data;
  623. if (success) {
  624. showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails));
  625. await refresh();
  626. } else {
  627. showError(message);
  628. }
  629. };
  630. // Test channel - 单个模型测试,参考旧版实现
  631. const testChannel = async (record, model) => {
  632. const testKey = `${record.id}-${model}`;
  633. // 检查是否应该停止批量测试
  634. if (shouldStopBatchTestingRef.current && isBatchTesting) {
  635. return Promise.resolve();
  636. }
  637. // 添加到正在测试的模型集合
  638. setTestingModels(prev => new Set([...prev, model]));
  639. try {
  640. const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
  641. // 检查是否在请求期间被停止
  642. if (shouldStopBatchTestingRef.current && isBatchTesting) {
  643. return Promise.resolve();
  644. }
  645. const { success, message, time } = res.data;
  646. // 更新测试结果
  647. setModelTestResults(prev => ({
  648. ...prev,
  649. [testKey]: {
  650. success,
  651. message,
  652. time: time || 0,
  653. timestamp: Date.now()
  654. }
  655. }));
  656. if (success) {
  657. // 更新渠道响应时间
  658. updateChannelProperty(record.id, (channel) => {
  659. channel.response_time = time * 1000;
  660. channel.test_time = Date.now() / 1000;
  661. });
  662. if (!model || model === '') {
  663. showInfo(
  664. t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。')
  665. .replace('${name}', record.name)
  666. .replace('${time.toFixed(2)}', time.toFixed(2)),
  667. );
  668. } else {
  669. showInfo(
  670. t('通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。')
  671. .replace('${name}', record.name)
  672. .replace('${model}', model)
  673. .replace('${time.toFixed(2)}', time.toFixed(2)),
  674. );
  675. }
  676. } else {
  677. showError(`${t('模型')} ${model}: ${message}`);
  678. }
  679. } catch (error) {
  680. // 处理网络错误
  681. const testKey = `${record.id}-${model}`;
  682. setModelTestResults(prev => ({
  683. ...prev,
  684. [testKey]: {
  685. success: false,
  686. message: error.message || t('网络错误'),
  687. time: 0,
  688. timestamp: Date.now()
  689. }
  690. }));
  691. showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`);
  692. } finally {
  693. // 从正在测试的模型集合中移除
  694. setTestingModels(prev => {
  695. const newSet = new Set(prev);
  696. newSet.delete(model);
  697. return newSet;
  698. });
  699. }
  700. };
  701. // 批量测试单个渠道的所有模型,参考旧版实现
  702. const batchTestModels = async () => {
  703. if (!currentTestChannel || !currentTestChannel.models) {
  704. showError(t('渠道模型信息不完整'));
  705. return;
  706. }
  707. const models = currentTestChannel.models.split(',').filter(model =>
  708. model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
  709. );
  710. if (models.length === 0) {
  711. showError(t('没有找到匹配的模型'));
  712. return;
  713. }
  714. setIsBatchTesting(true);
  715. shouldStopBatchTestingRef.current = false; // 重置停止标志
  716. // 清空该渠道之前的测试结果
  717. setModelTestResults(prev => {
  718. const newResults = { ...prev };
  719. models.forEach(model => {
  720. const testKey = `${currentTestChannel.id}-${model}`;
  721. delete newResults[testKey];
  722. });
  723. return newResults;
  724. });
  725. try {
  726. showInfo(t('开始批量测试 ${count} 个模型,已清空上次结果...').replace('${count}', models.length));
  727. // 提高并发数量以加快测试速度,参考旧版的并发限制
  728. const concurrencyLimit = 5;
  729. const results = [];
  730. for (let i = 0; i < models.length; i += concurrencyLimit) {
  731. // 检查是否应该停止
  732. if (shouldStopBatchTestingRef.current) {
  733. showInfo(t('批量测试已停止'));
  734. break;
  735. }
  736. const batch = models.slice(i, i + concurrencyLimit);
  737. showInfo(t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)')
  738. .replace('${current}', i + 1)
  739. .replace('${end}', Math.min(i + concurrencyLimit, models.length))
  740. .replace('${total}', models.length)
  741. );
  742. const batchPromises = batch.map(model => testChannel(currentTestChannel, model));
  743. const batchResults = await Promise.allSettled(batchPromises);
  744. results.push(...batchResults);
  745. // 再次检查是否应该停止
  746. if (shouldStopBatchTestingRef.current) {
  747. showInfo(t('批量测试已停止'));
  748. break;
  749. }
  750. // 短暂延迟避免过于频繁的请求
  751. if (i + concurrencyLimit < models.length) {
  752. await new Promise(resolve => setTimeout(resolve, 100));
  753. }
  754. }
  755. if (!shouldStopBatchTestingRef.current) {
  756. // 等待一小段时间确保所有结果都已更新
  757. await new Promise(resolve => setTimeout(resolve, 300));
  758. // 使用当前状态重新计算结果统计
  759. setModelTestResults(currentResults => {
  760. let successCount = 0;
  761. let failCount = 0;
  762. models.forEach(model => {
  763. const testKey = `${currentTestChannel.id}-${model}`;
  764. const result = currentResults[testKey];
  765. if (result && result.success) {
  766. successCount++;
  767. } else {
  768. failCount++;
  769. }
  770. });
  771. // 显示完成消息
  772. setTimeout(() => {
  773. showSuccess(t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}')
  774. .replace('${success}', successCount)
  775. .replace('${fail}', failCount)
  776. .replace('${total}', models.length)
  777. );
  778. }, 100);
  779. return currentResults; // 不修改状态,只是为了获取最新值
  780. });
  781. }
  782. } catch (error) {
  783. showError(t('批量测试过程中发生错误: ') + error.message);
  784. } finally {
  785. setIsBatchTesting(false);
  786. }
  787. };
  788. // 停止批量测试
  789. const stopBatchTesting = () => {
  790. shouldStopBatchTestingRef.current = true;
  791. setIsBatchTesting(false);
  792. setTestingModels(new Set());
  793. showInfo(t('已停止批量测试'));
  794. };
  795. // 清空测试结果
  796. const clearTestResults = () => {
  797. setModelTestResults({});
  798. showInfo(t('已清空测试结果'));
  799. };
  800. // Handle close modal
  801. const handleCloseModal = () => {
  802. // 如果正在批量测试,先停止测试
  803. if (isBatchTesting) {
  804. shouldStopBatchTestingRef.current = true;
  805. showInfo(t('关闭弹窗,已停止批量测试'));
  806. }
  807. setShowModelTestModal(false);
  808. setModelSearchKeyword('');
  809. setIsBatchTesting(false);
  810. setTestingModels(new Set());
  811. setSelectedModelKeys([]);
  812. setModelTablePage(1);
  813. // 可选择性保留测试结果,这里不清空以便用户查看
  814. };
  815. // Type counts
  816. const channelTypeCounts = useMemo(() => {
  817. if (Object.keys(typeCounts).length > 0) return typeCounts;
  818. const counts = { all: channels.length };
  819. channels.forEach((channel) => {
  820. const collect = (ch) => {
  821. const type = ch.type;
  822. counts[type] = (counts[type] || 0) + 1;
  823. };
  824. if (channel.children !== undefined) {
  825. channel.children.forEach(collect);
  826. } else {
  827. collect(channel);
  828. }
  829. });
  830. return counts;
  831. }, [typeCounts, channels]);
  832. const availableTypeKeys = useMemo(() => {
  833. const keys = ['all'];
  834. Object.entries(channelTypeCounts).forEach(([k, v]) => {
  835. if (k !== 'all' && v > 0) keys.push(String(k));
  836. });
  837. return keys;
  838. }, [channelTypeCounts]);
  839. return {
  840. // Basic states
  841. channels,
  842. loading,
  843. searching,
  844. activePage,
  845. pageSize,
  846. channelCount,
  847. groupOptions,
  848. idSort,
  849. enableTagMode,
  850. enableBatchDelete,
  851. statusFilter,
  852. compactMode,
  853. // UI states
  854. showEdit,
  855. setShowEdit,
  856. editingChannel,
  857. setEditingChannel,
  858. showEditTag,
  859. setShowEditTag,
  860. editingTag,
  861. setEditingTag,
  862. selectedChannels,
  863. setSelectedChannels,
  864. showBatchSetTag,
  865. setShowBatchSetTag,
  866. batchSetTagValue,
  867. setBatchSetTagValue,
  868. // Column states
  869. visibleColumns,
  870. showColumnSelector,
  871. setShowColumnSelector,
  872. COLUMN_KEYS,
  873. // Type tab states
  874. activeTypeKey,
  875. setActiveTypeKey,
  876. typeCounts,
  877. channelTypeCounts,
  878. availableTypeKeys,
  879. // Model test states
  880. showModelTestModal,
  881. setShowModelTestModal,
  882. currentTestChannel,
  883. setCurrentTestChannel,
  884. modelSearchKeyword,
  885. setModelSearchKeyword,
  886. modelTestResults,
  887. testingModels,
  888. selectedModelKeys,
  889. setSelectedModelKeys,
  890. isBatchTesting,
  891. modelTablePage,
  892. setModelTablePage,
  893. allSelectingRef,
  894. // Multi-key management states
  895. showMultiKeyManageModal,
  896. setShowMultiKeyManageModal,
  897. currentMultiKeyChannel,
  898. setCurrentMultiKeyChannel,
  899. // Form
  900. formApi,
  901. setFormApi,
  902. formInitValues,
  903. // Helpers
  904. t,
  905. isMobile,
  906. // Functions
  907. loadChannels,
  908. searchChannels,
  909. refresh,
  910. manageChannel,
  911. manageTag,
  912. handlePageChange,
  913. handlePageSizeChange,
  914. copySelectedChannel,
  915. updateChannelProperty,
  916. submitTagEdit,
  917. closeEdit,
  918. handleRow,
  919. batchSetChannelTag,
  920. batchDeleteChannels,
  921. testAllChannels,
  922. deleteAllDisabledChannels,
  923. updateAllChannelsBalance,
  924. updateChannelBalance,
  925. fixChannelsAbilities,
  926. testChannel,
  927. batchTestModels,
  928. handleCloseModal,
  929. getFormValues,
  930. // Column functions
  931. handleColumnVisibilityChange,
  932. handleSelectAll,
  933. initDefaultColumns,
  934. getDefaultColumnVisibility,
  935. // Setters
  936. setIdSort,
  937. setEnableTagMode,
  938. setEnableBatchDelete,
  939. setStatusFilter,
  940. setCompactMode,
  941. setActivePage,
  942. };
  943. };