ChannelsColumnDefs.jsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  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 React from 'react';
  16. import {
  17. Button,
  18. Dropdown,
  19. InputNumber,
  20. Modal,
  21. Space,
  22. SplitButtonGroup,
  23. Tag,
  24. Tooltip,
  25. Typography,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. timestamp2string,
  29. renderGroup,
  30. renderQuota,
  31. getChannelIcon,
  32. renderQuotaWithAmount,
  33. showSuccess,
  34. showError,
  35. showInfo,
  36. } from '../../../helpers';
  37. import {
  38. CHANNEL_OPTIONS,
  39. MODEL_FETCHABLE_CHANNEL_TYPES,
  40. } from '../../../constants';
  41. import { parseUpstreamUpdateMeta } from '../../../hooks/channels/upstreamUpdateUtils';
  42. import {
  43. IconTreeTriangleDown,
  44. IconMore,
  45. IconAlertTriangle,
  46. } from '@douyinfe/semi-icons';
  47. import { FaRandom } from 'react-icons/fa';
  48. // Render functions
  49. const renderType = (type, record = {}, t) => {
  50. const channelInfo = record?.channel_info;
  51. let type2label = new Map();
  52. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  53. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  54. }
  55. type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
  56. let icon = getChannelIcon(type);
  57. if (channelInfo?.is_multi_key) {
  58. icon =
  59. channelInfo?.multi_key_mode === 'random' ? (
  60. <div className='flex items-center gap-1'>
  61. <FaRandom className='text-blue-500' />
  62. {icon}
  63. </div>
  64. ) : (
  65. <div className='flex items-center gap-1'>
  66. <IconTreeTriangleDown className='text-blue-500' />
  67. {icon}
  68. </div>
  69. );
  70. }
  71. const typeTag = (
  72. <Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
  73. {type2label[type]?.label}
  74. </Tag>
  75. );
  76. let ionetMeta = null;
  77. if (record?.other_info) {
  78. try {
  79. const parsed = JSON.parse(record.other_info);
  80. if (parsed && typeof parsed === 'object' && parsed.source === 'ionet') {
  81. ionetMeta = parsed;
  82. }
  83. } catch (error) {
  84. // ignore invalid metadata
  85. }
  86. }
  87. if (!ionetMeta) {
  88. return typeTag;
  89. }
  90. const handleNavigate = (event) => {
  91. event?.stopPropagation?.();
  92. if (!ionetMeta?.deployment_id) {
  93. return;
  94. }
  95. const targetUrl = `/console/deployment?deployment_id=${ionetMeta.deployment_id}`;
  96. window.open(targetUrl, '_blank', 'noopener');
  97. };
  98. return (
  99. <Space spacing={6}>
  100. {typeTag}
  101. <Tooltip
  102. content={
  103. <div className='max-w-xs'>
  104. <div className='text-xs text-gray-600'>
  105. {t('来源于 IO.NET 部署')}
  106. </div>
  107. {ionetMeta?.deployment_id && (
  108. <div className='text-xs text-gray-500 mt-1'>
  109. {t('部署 ID')}: {ionetMeta.deployment_id}
  110. </div>
  111. )}
  112. </div>
  113. }
  114. >
  115. <span>
  116. <Tag
  117. color='purple'
  118. type='light'
  119. className='cursor-pointer'
  120. onClick={handleNavigate}
  121. >
  122. IO.NET
  123. </Tag>
  124. </span>
  125. </Tooltip>
  126. </Space>
  127. );
  128. };
  129. const renderTagType = (t) => {
  130. return (
  131. <Tag color='light-blue' shape='circle' type='light'>
  132. {t('标签聚合')}
  133. </Tag>
  134. );
  135. };
  136. const renderStatus = (status, channelInfo = undefined, t) => {
  137. if (channelInfo) {
  138. if (channelInfo.is_multi_key) {
  139. let keySize = channelInfo.multi_key_size;
  140. let enabledKeySize = keySize;
  141. if (channelInfo.multi_key_status_list) {
  142. enabledKeySize =
  143. keySize - Object.keys(channelInfo.multi_key_status_list).length;
  144. }
  145. return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
  146. }
  147. }
  148. switch (status) {
  149. case 1:
  150. return (
  151. <Tag color='green' shape='circle'>
  152. {t('已启用')}
  153. </Tag>
  154. );
  155. case 2:
  156. return (
  157. <Tag color='red' shape='circle'>
  158. {t('已禁用')}
  159. </Tag>
  160. );
  161. case 3:
  162. return (
  163. <Tag color='yellow' shape='circle'>
  164. {t('自动禁用')}
  165. </Tag>
  166. );
  167. default:
  168. return (
  169. <Tag color='grey' shape='circle'>
  170. {t('未知状态')}
  171. </Tag>
  172. );
  173. }
  174. };
  175. const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
  176. switch (status) {
  177. case 1:
  178. return (
  179. <Tag color='green' shape='circle'>
  180. {t('已启用')} {enabledKeySize}/{keySize}
  181. </Tag>
  182. );
  183. case 2:
  184. return (
  185. <Tag color='red' shape='circle'>
  186. {t('已禁用')} {enabledKeySize}/{keySize}
  187. </Tag>
  188. );
  189. case 3:
  190. return (
  191. <Tag color='yellow' shape='circle'>
  192. {t('自动禁用')} {enabledKeySize}/{keySize}
  193. </Tag>
  194. );
  195. default:
  196. return (
  197. <Tag color='grey' shape='circle'>
  198. {t('未知状态')} {enabledKeySize}/{keySize}
  199. </Tag>
  200. );
  201. }
  202. };
  203. const renderResponseTime = (responseTime, t) => {
  204. let time = responseTime / 1000;
  205. time = time.toFixed(2) + t(' 秒');
  206. if (responseTime === 0) {
  207. return (
  208. <Tag color='grey' shape='circle'>
  209. {t('未测试')}
  210. </Tag>
  211. );
  212. } else if (responseTime <= 1000) {
  213. return (
  214. <Tag color='green' shape='circle'>
  215. {time}
  216. </Tag>
  217. );
  218. } else if (responseTime <= 3000) {
  219. return (
  220. <Tag color='lime' shape='circle'>
  221. {time}
  222. </Tag>
  223. );
  224. } else if (responseTime <= 5000) {
  225. return (
  226. <Tag color='yellow' shape='circle'>
  227. {time}
  228. </Tag>
  229. );
  230. } else {
  231. return (
  232. <Tag color='red' shape='circle'>
  233. {time}
  234. </Tag>
  235. );
  236. }
  237. };
  238. const isRequestPassThroughEnabled = (record) => {
  239. if (!record || record.children !== undefined) {
  240. return false;
  241. }
  242. const settingValue = record.setting;
  243. if (!settingValue) {
  244. return false;
  245. }
  246. if (typeof settingValue === 'object') {
  247. return settingValue.pass_through_body_enabled === true;
  248. }
  249. if (typeof settingValue !== 'string') {
  250. return false;
  251. }
  252. try {
  253. const parsed = JSON.parse(settingValue);
  254. return parsed?.pass_through_body_enabled === true;
  255. } catch (error) {
  256. return false;
  257. }
  258. };
  259. const getUpstreamUpdateMeta = (record) => {
  260. const supported =
  261. !!record &&
  262. record.children === undefined &&
  263. MODEL_FETCHABLE_CHANNEL_TYPES.has(record.type);
  264. if (!record || record.children !== undefined) {
  265. return {
  266. supported: false,
  267. enabled: false,
  268. pendingAddModels: [],
  269. pendingRemoveModels: [],
  270. };
  271. }
  272. const parsed =
  273. record?.upstreamUpdateMeta && typeof record.upstreamUpdateMeta === 'object'
  274. ? record.upstreamUpdateMeta
  275. : parseUpstreamUpdateMeta(record?.settings);
  276. return {
  277. supported,
  278. enabled: parsed?.enabled === true,
  279. pendingAddModels: Array.isArray(parsed?.pendingAddModels)
  280. ? parsed.pendingAddModels
  281. : [],
  282. pendingRemoveModels: Array.isArray(parsed?.pendingRemoveModels)
  283. ? parsed.pendingRemoveModels
  284. : [],
  285. };
  286. };
  287. export const getChannelsColumns = ({
  288. t,
  289. COLUMN_KEYS,
  290. updateChannelBalance,
  291. manageChannel,
  292. manageTag,
  293. submitTagEdit,
  294. testChannel,
  295. setCurrentTestChannel,
  296. setShowModelTestModal,
  297. setEditingChannel,
  298. setShowEdit,
  299. setShowEditTag,
  300. setEditingTag,
  301. copySelectedChannel,
  302. refresh,
  303. activePage,
  304. channels,
  305. checkOllamaVersion,
  306. setShowMultiKeyManageModal,
  307. setCurrentMultiKeyChannel,
  308. openUpstreamUpdateModal,
  309. detectChannelUpstreamUpdates,
  310. }) => {
  311. return [
  312. {
  313. key: COLUMN_KEYS.ID,
  314. title: t('ID'),
  315. dataIndex: 'id',
  316. },
  317. {
  318. key: COLUMN_KEYS.NAME,
  319. title: t('名称'),
  320. dataIndex: 'name',
  321. render: (text, record, index) => {
  322. const passThroughEnabled = isRequestPassThroughEnabled(record);
  323. const upstreamUpdateMeta = getUpstreamUpdateMeta(record);
  324. const pendingAddCount = upstreamUpdateMeta.pendingAddModels.length;
  325. const pendingRemoveCount =
  326. upstreamUpdateMeta.pendingRemoveModels.length;
  327. const showUpstreamUpdateTag =
  328. upstreamUpdateMeta.supported &&
  329. upstreamUpdateMeta.enabled &&
  330. (pendingAddCount > 0 || pendingRemoveCount > 0);
  331. const nameNode =
  332. record.remark && record.remark.trim() !== '' ? (
  333. <Tooltip
  334. content={
  335. <div className='flex flex-col gap-2 max-w-xs'>
  336. <div className='text-sm'>{record.remark}</div>
  337. <Button
  338. size='small'
  339. type='primary'
  340. theme='outline'
  341. onClick={(e) => {
  342. e.stopPropagation();
  343. navigator.clipboard
  344. .writeText(record.remark)
  345. .then(() => {
  346. showSuccess(t('复制成功'));
  347. })
  348. .catch(() => {
  349. showError(t('复制失败'));
  350. });
  351. }}
  352. >
  353. {t('复制')}
  354. </Button>
  355. </div>
  356. }
  357. trigger='hover'
  358. position='topLeft'
  359. >
  360. <span>{text}</span>
  361. </Tooltip>
  362. ) : (
  363. <span>{text}</span>
  364. );
  365. if (!passThroughEnabled && !showUpstreamUpdateTag) {
  366. return nameNode;
  367. }
  368. return (
  369. <Space spacing={6} align='center'>
  370. {nameNode}
  371. {passThroughEnabled && (
  372. <Tooltip
  373. content={t(
  374. '该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。',
  375. )}
  376. trigger='hover'
  377. position='topLeft'
  378. >
  379. <span className='inline-flex items-center'>
  380. <IconAlertTriangle
  381. style={{ color: 'var(--semi-color-warning)' }}
  382. />
  383. </span>
  384. </Tooltip>
  385. )}
  386. {showUpstreamUpdateTag && (
  387. <Space spacing={4} align='center'>
  388. {pendingAddCount > 0 ? (
  389. <Tooltip content={t('点击处理新增模型')} position='top'>
  390. <Tag
  391. color='green'
  392. type='light'
  393. size='small'
  394. shape='circle'
  395. className='cursor-pointer transition-all duration-150 hover:opacity-85 hover:-translate-y-px active:scale-95'
  396. onClick={(e) => {
  397. e.stopPropagation();
  398. openUpstreamUpdateModal(
  399. record,
  400. upstreamUpdateMeta.pendingAddModels,
  401. upstreamUpdateMeta.pendingRemoveModels,
  402. 'add',
  403. );
  404. }}
  405. >
  406. +{pendingAddCount}
  407. </Tag>
  408. </Tooltip>
  409. ) : null}
  410. {pendingRemoveCount > 0 ? (
  411. <Tooltip content={t('点击处理删除模型')} position='top'>
  412. <Tag
  413. color='red'
  414. type='light'
  415. size='small'
  416. shape='circle'
  417. className='cursor-pointer transition-all duration-150 hover:opacity-85 hover:-translate-y-px active:scale-95'
  418. onClick={(e) => {
  419. e.stopPropagation();
  420. openUpstreamUpdateModal(
  421. record,
  422. upstreamUpdateMeta.pendingAddModels,
  423. upstreamUpdateMeta.pendingRemoveModels,
  424. 'remove',
  425. );
  426. }}
  427. >
  428. -{pendingRemoveCount}
  429. </Tag>
  430. </Tooltip>
  431. ) : null}
  432. </Space>
  433. )}
  434. </Space>
  435. );
  436. },
  437. },
  438. {
  439. key: COLUMN_KEYS.GROUP,
  440. title: t('分组'),
  441. dataIndex: 'group',
  442. render: (text, record, index) => (
  443. <div>
  444. <Space spacing={2}>
  445. {text
  446. ?.split(',')
  447. .sort((a, b) => {
  448. if (a === 'default') return -1;
  449. if (b === 'default') return 1;
  450. return a.localeCompare(b);
  451. })
  452. .map((item, index) => renderGroup(item))}
  453. </Space>
  454. </div>
  455. ),
  456. },
  457. {
  458. key: COLUMN_KEYS.TYPE,
  459. title: t('类型'),
  460. dataIndex: 'type',
  461. render: (text, record, index) => {
  462. if (record.children === undefined) {
  463. return <>{renderType(text, record, t)}</>;
  464. } else {
  465. return <>{renderTagType(t)}</>;
  466. }
  467. },
  468. },
  469. {
  470. key: COLUMN_KEYS.STATUS,
  471. title: t('状态'),
  472. dataIndex: 'status',
  473. render: (text, record, index) => {
  474. if (text === 3) {
  475. if (record.other_info === '') {
  476. record.other_info = '{}';
  477. }
  478. let otherInfo = JSON.parse(record.other_info);
  479. let reason = otherInfo['status_reason'];
  480. let time = otherInfo['status_time'];
  481. return (
  482. <div>
  483. <Tooltip
  484. content={
  485. t('原因:') + reason + t(',时间:') + timestamp2string(time)
  486. }
  487. >
  488. {renderStatus(text, record.channel_info, t)}
  489. </Tooltip>
  490. </div>
  491. );
  492. } else {
  493. return renderStatus(text, record.channel_info, t);
  494. }
  495. },
  496. },
  497. {
  498. key: COLUMN_KEYS.RESPONSE_TIME,
  499. title: t('响应时间'),
  500. dataIndex: 'response_time',
  501. render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>,
  502. },
  503. {
  504. key: COLUMN_KEYS.BALANCE,
  505. title: t('已用/剩余'),
  506. dataIndex: 'expired_time',
  507. render: (text, record, index) => {
  508. if (record.children === undefined) {
  509. return (
  510. <div>
  511. <Space spacing={1}>
  512. <Tooltip content={t('已用额度')}>
  513. <Tag color='white' type='ghost' shape='circle'>
  514. {renderQuota(record.used_quota)}
  515. </Tag>
  516. </Tooltip>
  517. <Tooltip
  518. content={t('剩余额度$') + record.balance + t(',点击更新')}
  519. >
  520. <Tag
  521. color='white'
  522. type='ghost'
  523. shape='circle'
  524. onClick={() => updateChannelBalance(record)}
  525. >
  526. {renderQuotaWithAmount(record.balance)}
  527. </Tag>
  528. </Tooltip>
  529. </Space>
  530. </div>
  531. );
  532. } else {
  533. return (
  534. <Tooltip content={t('已用额度')}>
  535. <Tag color='white' type='ghost' shape='circle'>
  536. {renderQuota(record.used_quota)}
  537. </Tag>
  538. </Tooltip>
  539. );
  540. }
  541. },
  542. },
  543. {
  544. key: COLUMN_KEYS.PRIORITY,
  545. title: t('优先级'),
  546. dataIndex: 'priority',
  547. render: (text, record, index) => {
  548. if (record.children === undefined) {
  549. return (
  550. <div>
  551. <InputNumber
  552. style={{ width: 70 }}
  553. name='priority'
  554. onBlur={(e) => {
  555. manageChannel(record.id, 'priority', record, e.target.value);
  556. }}
  557. keepFocus={true}
  558. innerButtons
  559. defaultValue={record.priority}
  560. min={-999}
  561. size='small'
  562. />
  563. </div>
  564. );
  565. } else {
  566. return (
  567. <InputNumber
  568. style={{ width: 70 }}
  569. name='priority'
  570. keepFocus={true}
  571. onBlur={(e) => {
  572. Modal.warning({
  573. title: t('修改子渠道优先级'),
  574. content:
  575. t('确定要修改所有子渠道优先级为 ') +
  576. e.target.value +
  577. t(' 吗?'),
  578. onOk: () => {
  579. if (e.target.value === '') {
  580. return;
  581. }
  582. submitTagEdit('priority', {
  583. tag: record.key,
  584. priority: e.target.value,
  585. });
  586. },
  587. });
  588. }}
  589. innerButtons
  590. defaultValue={record.priority}
  591. min={-999}
  592. size='small'
  593. />
  594. );
  595. }
  596. },
  597. },
  598. {
  599. key: COLUMN_KEYS.WEIGHT,
  600. title: t('权重'),
  601. dataIndex: 'weight',
  602. render: (text, record, index) => {
  603. if (record.children === undefined) {
  604. return (
  605. <div>
  606. <InputNumber
  607. style={{ width: 70 }}
  608. name='weight'
  609. onBlur={(e) => {
  610. manageChannel(record.id, 'weight', record, e.target.value);
  611. }}
  612. keepFocus={true}
  613. innerButtons
  614. defaultValue={record.weight}
  615. min={0}
  616. size='small'
  617. />
  618. </div>
  619. );
  620. } else {
  621. return (
  622. <InputNumber
  623. style={{ width: 70 }}
  624. name='weight'
  625. keepFocus={true}
  626. onBlur={(e) => {
  627. Modal.warning({
  628. title: t('修改子渠道权重'),
  629. content:
  630. t('确定要修改所有子渠道权重为 ') +
  631. e.target.value +
  632. t(' 吗?'),
  633. onOk: () => {
  634. if (e.target.value === '') {
  635. return;
  636. }
  637. submitTagEdit('weight', {
  638. tag: record.key,
  639. weight: e.target.value,
  640. });
  641. },
  642. });
  643. }}
  644. innerButtons
  645. defaultValue={record.weight}
  646. min={-999}
  647. size='small'
  648. />
  649. );
  650. }
  651. },
  652. },
  653. {
  654. key: COLUMN_KEYS.OPERATE,
  655. title: '',
  656. dataIndex: 'operate',
  657. fixed: 'right',
  658. render: (text, record, index) => {
  659. if (record.children === undefined) {
  660. const upstreamUpdateMeta = getUpstreamUpdateMeta(record);
  661. const moreMenuItems = [
  662. {
  663. node: 'item',
  664. name: t('删除'),
  665. type: 'danger',
  666. onClick: () => {
  667. Modal.confirm({
  668. title: t('确定是否要删除此渠道?'),
  669. content: t('此修改将不可逆'),
  670. onOk: () => {
  671. (async () => {
  672. await manageChannel(record.id, 'delete', record);
  673. await refresh();
  674. setTimeout(() => {
  675. if (channels.length === 0 && activePage > 1) {
  676. refresh(activePage - 1);
  677. }
  678. }, 100);
  679. })();
  680. },
  681. });
  682. },
  683. },
  684. {
  685. node: 'item',
  686. name: t('复制'),
  687. type: 'tertiary',
  688. onClick: () => {
  689. Modal.confirm({
  690. title: t('确定是否要复制此渠道?'),
  691. content: t('复制渠道的所有信息'),
  692. onOk: () => copySelectedChannel(record),
  693. });
  694. },
  695. },
  696. ];
  697. if (upstreamUpdateMeta.supported) {
  698. moreMenuItems.push({
  699. node: 'item',
  700. name: t('仅检测上游模型更新'),
  701. type: 'tertiary',
  702. onClick: () => {
  703. if (!upstreamUpdateMeta.enabled) {
  704. showInfo(t('该渠道未开启上游模型更新检测'));
  705. return;
  706. }
  707. detectChannelUpstreamUpdates(record);
  708. },
  709. });
  710. moreMenuItems.push({
  711. node: 'item',
  712. name: t('处理上游模型更新'),
  713. type: 'tertiary',
  714. onClick: () => {
  715. if (!upstreamUpdateMeta.enabled) {
  716. showInfo(t('该渠道未开启上游模型更新检测'));
  717. return;
  718. }
  719. if (
  720. upstreamUpdateMeta.pendingAddModels.length === 0 &&
  721. upstreamUpdateMeta.pendingRemoveModels.length === 0
  722. ) {
  723. showInfo(t('该渠道暂无可处理的上游模型更新'));
  724. return;
  725. }
  726. openUpstreamUpdateModal(
  727. record,
  728. upstreamUpdateMeta.pendingAddModels,
  729. upstreamUpdateMeta.pendingRemoveModels,
  730. upstreamUpdateMeta.pendingAddModels.length > 0
  731. ? 'add'
  732. : 'remove',
  733. );
  734. },
  735. });
  736. }
  737. if (record.type === 4) {
  738. moreMenuItems.unshift({
  739. node: 'item',
  740. name: t('测活'),
  741. type: 'tertiary',
  742. onClick: () => checkOllamaVersion(record),
  743. });
  744. }
  745. return (
  746. <Space wrap>
  747. <SplitButtonGroup
  748. className='overflow-hidden'
  749. aria-label={t('测试单个渠道操作项目组')}
  750. >
  751. <Button
  752. size='small'
  753. type='tertiary'
  754. onClick={() => testChannel(record, '')}
  755. >
  756. {t('测试')}
  757. </Button>
  758. <Button
  759. size='small'
  760. type='tertiary'
  761. icon={<IconTreeTriangleDown />}
  762. onClick={() => {
  763. setCurrentTestChannel(record);
  764. setShowModelTestModal(true);
  765. }}
  766. />
  767. </SplitButtonGroup>
  768. {record.status === 1 ? (
  769. <Button
  770. type='danger'
  771. size='small'
  772. onClick={() => manageChannel(record.id, 'disable', record)}
  773. >
  774. {t('禁用')}
  775. </Button>
  776. ) : (
  777. <Button
  778. size='small'
  779. onClick={() => manageChannel(record.id, 'enable', record)}
  780. >
  781. {t('启用')}
  782. </Button>
  783. )}
  784. {record.channel_info?.is_multi_key ? (
  785. <SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}>
  786. <Button
  787. type='tertiary'
  788. size='small'
  789. onClick={() => {
  790. setEditingChannel(record);
  791. setShowEdit(true);
  792. }}
  793. >
  794. {t('编辑')}
  795. </Button>
  796. <Dropdown
  797. trigger='click'
  798. position='bottomRight'
  799. menu={[
  800. {
  801. node: 'item',
  802. name: t('多密钥管理'),
  803. onClick: () => {
  804. setCurrentMultiKeyChannel(record);
  805. setShowMultiKeyManageModal(true);
  806. },
  807. },
  808. ]}
  809. >
  810. <Button
  811. type='tertiary'
  812. size='small'
  813. icon={<IconTreeTriangleDown />}
  814. />
  815. </Dropdown>
  816. </SplitButtonGroup>
  817. ) : (
  818. <Button
  819. type='tertiary'
  820. size='small'
  821. onClick={() => {
  822. setEditingChannel(record);
  823. setShowEdit(true);
  824. }}
  825. >
  826. {t('编辑')}
  827. </Button>
  828. )}
  829. <Dropdown
  830. trigger='click'
  831. position='bottomRight'
  832. menu={moreMenuItems}
  833. >
  834. <Button icon={<IconMore />} type='tertiary' size='small' />
  835. </Dropdown>
  836. </Space>
  837. );
  838. } else {
  839. // 标签操作按钮
  840. return (
  841. <Space wrap>
  842. <Button
  843. type='tertiary'
  844. size='small'
  845. onClick={() => manageTag(record.key, 'enable')}
  846. >
  847. {t('启用全部')}
  848. </Button>
  849. <Button
  850. type='tertiary'
  851. size='small'
  852. onClick={() => manageTag(record.key, 'disable')}
  853. >
  854. {t('禁用全部')}
  855. </Button>
  856. <Button
  857. type='tertiary'
  858. size='small'
  859. onClick={() => {
  860. setShowEditTag(true);
  861. setEditingTag(record.key);
  862. }}
  863. >
  864. {t('编辑')}
  865. </Button>
  866. </Space>
  867. );
  868. }
  869. },
  870. },
  871. ];
  872. };