useChannelsData.jsx 32 KB

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