useChannelsData.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  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/index.js';
  25. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants/index.js';
  26. import { useIsMobile } from '../common/useIsMobile.js';
  27. import { useTableCompactMode } from '../common/useTableCompactMode.js';
  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 [testQueue, setTestQueue] = useState([]);
  71. const [isProcessingQueue, setIsProcessingQueue] = useState(false);
  72. const [modelTablePage, setModelTablePage] = useState(1);
  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. setTestQueue(prev => [...prev, { channel: record, model }]);
  633. if (!isProcessingQueue) {
  634. setIsProcessingQueue(true);
  635. }
  636. };
  637. // Process test queue
  638. const processTestQueue = async () => {
  639. if (!isProcessingQueue || testQueue.length === 0) return;
  640. const { channel, model, indexInFiltered } = testQueue[0];
  641. if (currentTestChannel && currentTestChannel.id === channel.id) {
  642. let pageNo;
  643. if (indexInFiltered !== undefined) {
  644. pageNo = Math.floor(indexInFiltered / MODEL_TABLE_PAGE_SIZE) + 1;
  645. } else {
  646. const filteredModelsList = currentTestChannel.models
  647. .split(',')
  648. .filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()));
  649. const modelIdx = filteredModelsList.indexOf(model);
  650. pageNo = modelIdx !== -1 ? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1 : 1;
  651. }
  652. setModelTablePage(pageNo);
  653. }
  654. try {
  655. setTestingModels(prev => new Set([...prev, model]));
  656. const res = await API.get(`/api/channel/test/${channel.id}?model=${model}`);
  657. const { success, message, time } = res.data;
  658. setModelTestResults(prev => ({
  659. ...prev,
  660. [`${channel.id}-${model}`]: { success, time }
  661. }));
  662. if (success) {
  663. updateChannelProperty(channel.id, (ch) => {
  664. ch.response_time = time * 1000;
  665. ch.test_time = Date.now() / 1000;
  666. });
  667. if (!model) {
  668. showInfo(
  669. t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。')
  670. .replace('${name}', channel.name)
  671. .replace('${time.toFixed(2)}', time.toFixed(2)),
  672. );
  673. }
  674. } else {
  675. showError(message);
  676. }
  677. } catch (error) {
  678. showError(error.message);
  679. } finally {
  680. setTestingModels(prev => {
  681. const newSet = new Set(prev);
  682. newSet.delete(model);
  683. return newSet;
  684. });
  685. }
  686. setTestQueue(prev => prev.slice(1));
  687. };
  688. // Monitor queue changes
  689. useEffect(() => {
  690. if (testQueue.length > 0 && isProcessingQueue) {
  691. processTestQueue();
  692. } else if (testQueue.length === 0 && isProcessingQueue) {
  693. setIsProcessingQueue(false);
  694. setIsBatchTesting(false);
  695. }
  696. }, [testQueue, isProcessingQueue]);
  697. // Batch test models
  698. const batchTestModels = async () => {
  699. if (!currentTestChannel) return;
  700. setIsBatchTesting(true);
  701. setModelTablePage(1);
  702. const filteredModels = currentTestChannel.models
  703. .split(',')
  704. .filter((model) =>
  705. model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
  706. );
  707. setTestQueue(
  708. filteredModels.map((model, idx) => ({
  709. channel: currentTestChannel,
  710. model,
  711. indexInFiltered: idx,
  712. })),
  713. );
  714. setIsProcessingQueue(true);
  715. };
  716. // Handle close modal
  717. const handleCloseModal = () => {
  718. if (isBatchTesting) {
  719. setTestQueue([]);
  720. setIsProcessingQueue(false);
  721. setIsBatchTesting(false);
  722. showSuccess(t('已停止测试'));
  723. } else {
  724. setShowModelTestModal(false);
  725. setModelSearchKeyword('');
  726. setSelectedModelKeys([]);
  727. setModelTablePage(1);
  728. }
  729. };
  730. // Type counts
  731. const channelTypeCounts = useMemo(() => {
  732. if (Object.keys(typeCounts).length > 0) return typeCounts;
  733. const counts = { all: channels.length };
  734. channels.forEach((channel) => {
  735. const collect = (ch) => {
  736. const type = ch.type;
  737. counts[type] = (counts[type] || 0) + 1;
  738. };
  739. if (channel.children !== undefined) {
  740. channel.children.forEach(collect);
  741. } else {
  742. collect(channel);
  743. }
  744. });
  745. return counts;
  746. }, [typeCounts, channels]);
  747. const availableTypeKeys = useMemo(() => {
  748. const keys = ['all'];
  749. Object.entries(channelTypeCounts).forEach(([k, v]) => {
  750. if (k !== 'all' && v > 0) keys.push(String(k));
  751. });
  752. return keys;
  753. }, [channelTypeCounts]);
  754. return {
  755. // Basic states
  756. channels,
  757. loading,
  758. searching,
  759. activePage,
  760. pageSize,
  761. channelCount,
  762. groupOptions,
  763. idSort,
  764. enableTagMode,
  765. enableBatchDelete,
  766. statusFilter,
  767. compactMode,
  768. // UI states
  769. showEdit,
  770. setShowEdit,
  771. editingChannel,
  772. setEditingChannel,
  773. showEditTag,
  774. setShowEditTag,
  775. editingTag,
  776. setEditingTag,
  777. selectedChannels,
  778. setSelectedChannels,
  779. showBatchSetTag,
  780. setShowBatchSetTag,
  781. batchSetTagValue,
  782. setBatchSetTagValue,
  783. // Column states
  784. visibleColumns,
  785. showColumnSelector,
  786. setShowColumnSelector,
  787. COLUMN_KEYS,
  788. // Type tab states
  789. activeTypeKey,
  790. setActiveTypeKey,
  791. typeCounts,
  792. channelTypeCounts,
  793. availableTypeKeys,
  794. // Model test states
  795. showModelTestModal,
  796. setShowModelTestModal,
  797. currentTestChannel,
  798. setCurrentTestChannel,
  799. modelSearchKeyword,
  800. setModelSearchKeyword,
  801. modelTestResults,
  802. testingModels,
  803. selectedModelKeys,
  804. setSelectedModelKeys,
  805. isBatchTesting,
  806. modelTablePage,
  807. setModelTablePage,
  808. allSelectingRef,
  809. // Multi-key management states
  810. showMultiKeyManageModal,
  811. setShowMultiKeyManageModal,
  812. currentMultiKeyChannel,
  813. setCurrentMultiKeyChannel,
  814. // Form
  815. formApi,
  816. setFormApi,
  817. formInitValues,
  818. // Helpers
  819. t,
  820. isMobile,
  821. // Functions
  822. loadChannels,
  823. searchChannels,
  824. refresh,
  825. manageChannel,
  826. manageTag,
  827. handlePageChange,
  828. handlePageSizeChange,
  829. copySelectedChannel,
  830. updateChannelProperty,
  831. submitTagEdit,
  832. closeEdit,
  833. handleRow,
  834. batchSetChannelTag,
  835. batchDeleteChannels,
  836. testAllChannels,
  837. deleteAllDisabledChannels,
  838. updateAllChannelsBalance,
  839. updateChannelBalance,
  840. fixChannelsAbilities,
  841. testChannel,
  842. batchTestModels,
  843. handleCloseModal,
  844. getFormValues,
  845. // Column functions
  846. handleColumnVisibilityChange,
  847. handleSelectAll,
  848. initDefaultColumns,
  849. getDefaultColumnVisibility,
  850. // Setters
  851. setIdSort,
  852. setEnableTagMode,
  853. setEnableBatchDelete,
  854. setStatusFilter,
  855. setCompactMode,
  856. setActivePage,
  857. };
  858. };