|
- /*
- Copyright (C) 2025 QuantumNous
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
- For commercial licensing, please contact support@quantumnous.com
- */
-
- import React from 'react';
- import {
- Button,
- Dropdown,
- InputNumber,
- Modal,
- Space,
- SplitButtonGroup,
- Tag,
- Tooltip,
- Typography,
- } from '@douyinfe/semi-ui';
- import {
- timestamp2string,
- renderGroup,
- renderQuota,
- getChannelIcon,
- renderQuotaWithAmount,
- showSuccess,
- showError,
- } from '../../../helpers';
- import { CHANNEL_OPTIONS } from '../../../constants';
- import {
- IconTreeTriangleDown,
- IconMore,
- IconAlertTriangle,
- } from '@douyinfe/semi-icons';
- import { FaRandom } from 'react-icons/fa';
-
- // Render functions
- const renderType = (type, record = {}, t) => {
- const channelInfo = record?.channel_info;
- let type2label = new Map();
- for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
- type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
- }
- type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
-
- let icon = getChannelIcon(type);
-
- if (channelInfo?.is_multi_key) {
- icon =
- channelInfo?.multi_key_mode === 'random' ? (
- <div className='flex items-center gap-1'>
- <FaRandom className='text-blue-500' />
- {icon}
- </div>
- ) : (
- <div className='flex items-center gap-1'>
- <IconTreeTriangleDown className='text-blue-500' />
- {icon}
- </div>
- );
- }
-
- const typeTag = (
- <Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
- {t(type2label[type]?.label)}
- </Tag>
- );
-
- let ionetMeta = null;
- if (record?.other_info) {
- try {
- const parsed = JSON.parse(record.other_info);
- if (parsed && typeof parsed === 'object' && parsed.source === 'ionet') {
- ionetMeta = parsed;
- }
- } catch (error) {
- // ignore invalid metadata
- }
- }
-
- if (!ionetMeta) {
- return typeTag;
- }
-
- const handleNavigate = (event) => {
- event?.stopPropagation?.();
- if (!ionetMeta?.deployment_id) {
- return;
- }
- const targetUrl = `/console/deployment?deployment_id=${ionetMeta.deployment_id}`;
- window.open(targetUrl, '_blank', 'noopener');
- };
-
- return (
- <Space spacing={6}>
- {typeTag}
- <Tooltip
- content={
- <div className='max-w-xs'>
- <div className='text-xs text-gray-600'>
- {t('来源于 IO.NET 部署')}
- </div>
- {ionetMeta?.deployment_id && (
- <div className='text-xs text-gray-500 mt-1'>
- {t('部署 ID')}: {ionetMeta.deployment_id}
- </div>
- )}
- </div>
- }
- >
- <span>
- <Tag
- color='purple'
- type='light'
- className='cursor-pointer'
- onClick={handleNavigate}
- >
- IO.NET
- </Tag>
- </span>
- </Tooltip>
- </Space>
- );
- };
-
- const renderTagType = (t) => {
- return (
- <Tag color='light-blue' shape='circle' type='light'>
- {t('标签聚合')}
- </Tag>
- );
- };
-
- const renderStatus = (status, channelInfo = undefined, t) => {
- if (channelInfo) {
- if (channelInfo.is_multi_key) {
- let keySize = channelInfo.multi_key_size;
- let enabledKeySize = keySize;
- if (channelInfo.multi_key_status_list) {
- enabledKeySize =
- keySize - Object.keys(channelInfo.multi_key_status_list).length;
- }
- return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
- }
- }
- switch (status) {
- case 1:
- return (
- <Tag color='green' shape='circle'>
- {t('已启用')}
- </Tag>
- );
- case 2:
- return (
- <Tag color='red' shape='circle'>
- {t('已禁用')}
- </Tag>
- );
- case 3:
- return (
- <Tag color='yellow' shape='circle'>
- {t('自动禁用')}
- </Tag>
- );
- default:
- return (
- <Tag color='grey' shape='circle'>
- {t('未知状态')}
- </Tag>
- );
- }
- };
-
- const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
- switch (status) {
- case 1:
- return (
- <Tag color='green' shape='circle'>
- {t('已启用')} {enabledKeySize}/{keySize}
- </Tag>
- );
- case 2:
- return (
- <Tag color='red' shape='circle'>
- {t('已禁用')} {enabledKeySize}/{keySize}
- </Tag>
- );
- case 3:
- return (
- <Tag color='yellow' shape='circle'>
- {t('自动禁用')} {enabledKeySize}/{keySize}
- </Tag>
- );
- default:
- return (
- <Tag color='grey' shape='circle'>
- {t('未知状态')} {enabledKeySize}/{keySize}
- </Tag>
- );
- }
- };
-
- const renderResponseTime = (responseTime, t) => {
- let time = responseTime / 1000;
- time = time.toFixed(2) + t(' 秒');
- if (responseTime === 0) {
- return (
- <Tag color='grey' shape='circle'>
- {t('未测试')}
- </Tag>
- );
- } else if (responseTime <= 1000) {
- return (
- <Tag color='green' shape='circle'>
- {time}
- </Tag>
- );
- } else if (responseTime <= 3000) {
- return (
- <Tag color='lime' shape='circle'>
- {time}
- </Tag>
- );
- } else if (responseTime <= 5000) {
- return (
- <Tag color='yellow' shape='circle'>
- {time}
- </Tag>
- );
- } else {
- return (
- <Tag color='red' shape='circle'>
- {time}
- </Tag>
- );
- }
- };
-
- const isRequestPassThroughEnabled = (record) => {
- if (!record || record.children !== undefined) {
- return false;
- }
- const settingValue = record.setting;
- if (!settingValue) {
- return false;
- }
- if (typeof settingValue === 'object') {
- return settingValue.pass_through_body_enabled === true;
- }
- if (typeof settingValue !== 'string') {
- return false;
- }
- try {
- const parsed = JSON.parse(settingValue);
- return parsed?.pass_through_body_enabled === true;
- } catch (error) {
- return false;
- }
- };
-
- export const getChannelsColumns = ({
- t,
- COLUMN_KEYS,
- updateChannelBalance,
- manageChannel,
- manageTag,
- submitTagEdit,
- testChannel,
- setCurrentTestChannel,
- setShowModelTestModal,
- setEditingChannel,
- setShowEdit,
- setShowEditTag,
- setEditingTag,
- copySelectedChannel,
- refresh,
- activePage,
- channels,
- checkOllamaVersion,
- setShowMultiKeyManageModal,
- setCurrentMultiKeyChannel,
- }) => {
- return [
- {
- key: COLUMN_KEYS.ID,
- title: t('ID'),
- dataIndex: 'id',
- },
- {
- key: COLUMN_KEYS.NAME,
- title: t('名称'),
- dataIndex: 'name',
- render: (text, record, index) => {
- const passThroughEnabled = isRequestPassThroughEnabled(record);
- const nameWithId = `${record.id} - ${text}`;
- const nameNode =
- record.remark && record.remark.trim() !== '' ? (
- <Tooltip
- content={
- <div className='flex flex-col gap-2 max-w-xs'>
- <div className='text-sm'>{record.remark}</div>
- <Button
- size='small'
- type='primary'
- theme='outline'
- onClick={(e) => {
- e.stopPropagation();
- navigator.clipboard
- .writeText(record.remark)
- .then(() => {
- showSuccess(t('复制成功'));
- })
- .catch(() => {
- showError(t('复制失败'));
- });
- }}
- >
- {t('复制')}
- </Button>
- </div>
- }
- trigger='hover'
- position='topLeft'
- >
- <span>{nameWithId}</span>
- </Tooltip>
- ) : (
- <span>{nameWithId}</span>
- );
-
- if (!passThroughEnabled) {
- return nameNode;
- }
-
- return (
- <Space spacing={6} align='center'>
- {nameNode}
- <Tooltip
- content={t(
- '该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。',
- )}
- trigger='hover'
- position='topLeft'
- >
- <span className='inline-flex items-center'>
- <IconAlertTriangle
- style={{ color: 'var(--semi-color-warning)' }}
- />
- </span>
- </Tooltip>
- </Space>
- );
- },
- },
- {
- key: COLUMN_KEYS.GROUP,
- title: t('分组'),
- dataIndex: 'group',
- render: (text, record, index) => (
- <div>
- <Space spacing={2}>
- {text
- ?.split(',')
- .sort((a, b) => {
- if (a === 'default') return -1;
- if (b === 'default') return 1;
- return a.localeCompare(b);
- })
- .map((item, index) => renderGroup(item))}
- </Space>
- </div>
- ),
- },
- {
- key: COLUMN_KEYS.TYPE,
- title: t('类型'),
- dataIndex: 'type',
- render: (text, record, index) => {
- if (record.children === undefined) {
- return <>{renderType(text, record, t)}</>;
- } else {
- return <>{renderTagType(t)}</>;
- }
- },
- },
- {
- key: COLUMN_KEYS.STATUS,
- title: t('状态'),
- dataIndex: 'status',
- render: (text, record, index) => {
- if (text === 3) {
- if (record.other_info === '') {
- record.other_info = '{}';
- }
- let otherInfo = JSON.parse(record.other_info);
- let reason = otherInfo['status_reason'];
- let time = otherInfo['status_time'];
- return (
- <div>
- <Tooltip
- content={
- t('原因:') + reason + t(',时间:') + timestamp2string(time)
- }
- >
- {renderStatus(text, record.channel_info, t)}
- </Tooltip>
- </div>
- );
- } else {
- return renderStatus(text, record.channel_info, t);
- }
- },
- },
- {
- key: COLUMN_KEYS.RESPONSE_TIME,
- title: t('响应时间'),
- dataIndex: 'response_time',
- render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>,
- },
- {
- key: COLUMN_KEYS.BALANCE,
- title: t('已用/剩余'),
- dataIndex: 'expired_time',
- render: (text, record, index) => {
- if (record.children === undefined) {
- return (
- <div>
- <Space spacing={1}>
- <Tooltip content={t('已用额度')}>
- <Tag color='white' type='ghost' shape='circle'>
- {renderQuota(record.used_quota)}
- </Tag>
- </Tooltip>
- <Tooltip
- content={t('剩余额度$') + record.balance + t(',点击更新')}
- >
- <Tag
- color='white'
- type='ghost'
- shape='circle'
- onClick={() => updateChannelBalance(record)}
- >
- {renderQuotaWithAmount(record.balance)}
- </Tag>
- </Tooltip>
- </Space>
- </div>
- );
- } else {
- return (
- <Tooltip content={t('已用额度')}>
- <Tag color='white' type='ghost' shape='circle'>
- {renderQuota(record.used_quota)}
- </Tag>
- </Tooltip>
- );
- }
- },
- },
- {
- key: COLUMN_KEYS.PRIORITY,
- title: t('优先级'),
- dataIndex: 'priority',
- render: (text, record, index) => {
- if (record.children === undefined) {
- return (
- <div>
- <InputNumber
- style={{ width: 70 }}
- name='priority'
- onBlur={(e) => {
- manageChannel(record.id, 'priority', record, e.target.value);
- }}
- keepFocus={true}
- innerButtons
- defaultValue={record.priority}
- min={-999}
- size='small'
- />
- </div>
- );
- } else {
- return (
- <InputNumber
- style={{ width: 70 }}
- name='priority'
- keepFocus={true}
- onBlur={(e) => {
- Modal.warning({
- title: t('修改子渠道优先级'),
- content:
- t('确定要修改所有子渠道优先级为 ') +
- e.target.value +
- t(' 吗?'),
- onOk: () => {
- if (e.target.value === '') {
- return;
- }
- submitTagEdit('priority', {
- tag: record.key,
- priority: e.target.value,
- });
- },
- });
- }}
- innerButtons
- defaultValue={record.priority}
- min={-999}
- size='small'
- />
- );
- }
- },
- },
- {
- key: COLUMN_KEYS.WEIGHT,
- title: t('权重'),
- dataIndex: 'weight',
- render: (text, record, index) => {
- if (record.children === undefined) {
- return (
- <div>
- <InputNumber
- style={{ width: 70 }}
- name='weight'
- onBlur={(e) => {
- manageChannel(record.id, 'weight', record, e.target.value);
- }}
- keepFocus={true}
- innerButtons
- defaultValue={record.weight}
- min={0}
- size='small'
- />
- </div>
- );
- } else {
- return (
- <InputNumber
- style={{ width: 70 }}
- name='weight'
- keepFocus={true}
- onBlur={(e) => {
- Modal.warning({
- title: t('修改子渠道权重'),
- content:
- t('确定要修改所有子渠道权重为 ') +
- e.target.value +
- t(' 吗?'),
- onOk: () => {
- if (e.target.value === '') {
- return;
- }
- submitTagEdit('weight', {
- tag: record.key,
- weight: e.target.value,
- });
- },
- });
- }}
- innerButtons
- defaultValue={record.weight}
- min={-999}
- size='small'
- />
- );
- }
- },
- },
- {
- key: COLUMN_KEYS.OPERATE,
- title: '',
- dataIndex: 'operate',
- fixed: 'right',
- render: (text, record, index) => {
- if (record.children === undefined) {
- const moreMenuItems = [
- {
- node: 'item',
- name: t('删除'),
- type: 'danger',
- onClick: () => {
- Modal.confirm({
- title: t('确定是否要删除此渠道?'),
- content: t('此修改将不可逆'),
- onOk: () => {
- (async () => {
- await manageChannel(record.id, 'delete', record);
- await refresh();
- setTimeout(() => {
- if (channels.length === 0 && activePage > 1) {
- refresh(activePage - 1);
- }
- }, 100);
- })();
- },
- });
- },
- },
- {
- node: 'item',
- name: t('复制'),
- type: 'tertiary',
- onClick: () => {
- Modal.confirm({
- title: t('确定是否要复制此渠道?'),
- content: t('复制渠道的所有信息'),
- onOk: () => copySelectedChannel(record),
- });
- },
- },
- ];
-
- if (record.type === 4) {
- moreMenuItems.unshift({
- node: 'item',
- name: t('测活'),
- type: 'tertiary',
- onClick: () => checkOllamaVersion(record),
- });
- }
-
- return (
- <Space wrap>
- <SplitButtonGroup
- className='overflow-hidden'
- aria-label={t('测试单个渠道操作项目组')}
- >
- <Button
- size='small'
- type='tertiary'
- onClick={() => testChannel(record, '')}
- >
- {t('测试')}
- </Button>
- <Button
- size='small'
- type='tertiary'
- icon={<IconTreeTriangleDown />}
- onClick={() => {
- setCurrentTestChannel(record);
- setShowModelTestModal(true);
- }}
- />
- </SplitButtonGroup>
-
- {record.status === 1 ? (
- <Button
- type='danger'
- size='small'
- onClick={() => manageChannel(record.id, 'disable', record)}
- >
- {t('禁用')}
- </Button>
- ) : (
- <Button
- size='small'
- onClick={() => manageChannel(record.id, 'enable', record)}
- >
- {t('启用')}
- </Button>
- )}
-
- {record.channel_info?.is_multi_key ? (
- <SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}>
- <Button
- type='tertiary'
- size='small'
- onClick={() => {
- setEditingChannel(record);
- setShowEdit(true);
- }}
- >
- {t('编辑')}
- </Button>
- <Dropdown
- trigger='click'
- position='bottomRight'
- menu={[
- {
- node: 'item',
- name: t('多密钥管理'),
- onClick: () => {
- setCurrentMultiKeyChannel(record);
- setShowMultiKeyManageModal(true);
- },
- },
- ]}
- >
- <Button
- type='tertiary'
- size='small'
- icon={<IconTreeTriangleDown />}
- />
- </Dropdown>
- </SplitButtonGroup>
- ) : (
- <Button
- type='tertiary'
- size='small'
- onClick={() => {
- setEditingChannel(record);
- setShowEdit(true);
- }}
- >
- {t('编辑')}
- </Button>
- )}
-
- <Dropdown
- trigger='click'
- position='bottomRight'
- menu={moreMenuItems}
- >
- <Button icon={<IconMore />} type='tertiary' size='small' />
- </Dropdown>
- </Space>
- );
- } else {
- // 标签操作按钮
- return (
- <Space wrap>
- <Button
- type='tertiary'
- size='small'
- onClick={() => manageTag(record.key, 'enable')}
- >
- {t('启用全部')}
- </Button>
- <Button
- type='tertiary'
- size='small'
- onClick={() => manageTag(record.key, 'disable')}
- >
- {t('禁用全部')}
- </Button>
- <Button
- type='tertiary'
- size='small'
- onClick={() => {
- setShowEditTag(true);
- setEditingTag(record.key);
- }}
- >
- {t('编辑')}
- </Button>
- </Space>
- );
- }
- },
- },
- ];
- };
|