useChannelsData.jsx 34 KB

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