| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React from 'react'; |
| import { |
| Avatar, |
| Space, |
| Tag, |
| Tooltip, |
| Popover, |
| Typography, |
| } from '@douyinfe/semi-ui'; |
| import { |
| timestamp2string, |
| renderGroup, |
| renderQuota, |
| stringToColor, |
| getLogOther, |
| renderModelTag, |
| renderClaudeLogContent, |
| renderLogContent, |
| renderModelPriceSimple, |
| renderAudioModelPrice, |
| renderClaudeModelPrice, |
| renderModelPrice, |
| } from '../../../helpers'; |
| import { IconHelpCircle } from '@douyinfe/semi-icons'; |
| import { Route } from 'lucide-react'; |
|
|
| const colors = [ |
| 'amber', |
| 'blue', |
| 'cyan', |
| 'green', |
| 'grey', |
| 'indigo', |
| 'light-blue', |
| 'lime', |
| 'orange', |
| 'pink', |
| 'purple', |
| 'red', |
| 'teal', |
| 'violet', |
| 'yellow', |
| ]; |
|
|
| |
| function renderType(type, t) { |
| switch (type) { |
| case 1: |
| return ( |
| <Tag color='cyan' shape='circle'> |
| {t('充值')} |
| </Tag> |
| ); |
| case 2: |
| return ( |
| <Tag color='lime' shape='circle'> |
| {t('消费')} |
| </Tag> |
| ); |
| case 3: |
| return ( |
| <Tag color='orange' shape='circle'> |
| {t('管理')} |
| </Tag> |
| ); |
| case 4: |
| return ( |
| <Tag color='purple' shape='circle'> |
| {t('系统')} |
| </Tag> |
| ); |
| case 5: |
| return ( |
| <Tag color='red' shape='circle'> |
| {t('错误')} |
| </Tag> |
| ); |
| default: |
| return ( |
| <Tag color='grey' shape='circle'> |
| {t('未知')} |
| </Tag> |
| ); |
| } |
| } |
|
|
| function renderIsStream(bool, t) { |
| if (bool) { |
| return ( |
| <Tag color='blue' shape='circle'> |
| {t('流')} |
| </Tag> |
| ); |
| } else { |
| return ( |
| <Tag color='purple' shape='circle'> |
| {t('非流')} |
| </Tag> |
| ); |
| } |
| } |
|
|
| function renderUseTime(type, t) { |
| const time = parseInt(type); |
| if (time < 101) { |
| return ( |
| <Tag color='green' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } else if (time < 300) { |
| return ( |
| <Tag color='orange' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } else { |
| return ( |
| <Tag color='red' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } |
| } |
|
|
| function renderFirstUseTime(type, t) { |
| let time = parseFloat(type) / 1000.0; |
| time = time.toFixed(1); |
| if (time < 3) { |
| return ( |
| <Tag color='green' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } else if (time < 10) { |
| return ( |
| <Tag color='orange' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } else { |
| return ( |
| <Tag color='red' shape='circle'> |
| {' '} |
| {time} s{' '} |
| </Tag> |
| ); |
| } |
| } |
|
|
| function renderModelName(record, copyText, t) { |
| let other = getLogOther(record.other); |
| let modelMapped = |
| other?.is_model_mapped && |
| other?.upstream_model_name && |
| other?.upstream_model_name !== ''; |
| if (!modelMapped) { |
| return renderModelTag(record.model_name, { |
| onClick: (event) => { |
| copyText(event, record.model_name).then((r) => {}); |
| }, |
| }); |
| } else { |
| return ( |
| <> |
| <Space vertical align={'start'}> |
| <Popover |
| content={ |
| <div style={{ padding: 10 }}> |
| <Space vertical align={'start'}> |
| <div className='flex items-center'> |
| <Typography.Text strong style={{ marginRight: 8 }}> |
| {t('请求并计费模型')}: |
| </Typography.Text> |
| {renderModelTag(record.model_name, { |
| onClick: (event) => { |
| copyText(event, record.model_name).then((r) => {}); |
| }, |
| })} |
| </div> |
| <div className='flex items-center'> |
| <Typography.Text strong style={{ marginRight: 8 }}> |
| {t('实际模型')}: |
| </Typography.Text> |
| {renderModelTag(other.upstream_model_name, { |
| onClick: (event) => { |
| copyText(event, other.upstream_model_name).then( |
| (r) => {}, |
| ); |
| }, |
| })} |
| </div> |
| </Space> |
| </div> |
| } |
| > |
| {renderModelTag(record.model_name, { |
| onClick: (event) => { |
| copyText(event, record.model_name).then((r) => {}); |
| }, |
| suffixIcon: ( |
| <Route |
| style={{ width: '0.9em', height: '0.9em', opacity: 0.75 }} |
| /> |
| ), |
| })} |
| </Popover> |
| </Space> |
| </> |
| ); |
| } |
| } |
|
|
| export const getLogsColumns = ({ |
| t, |
| COLUMN_KEYS, |
| copyText, |
| showUserInfoFunc, |
| isAdminUser, |
| }) => { |
| return [ |
| { |
| key: COLUMN_KEYS.TIME, |
| title: t('时间'), |
| dataIndex: 'timestamp2string', |
| }, |
| { |
| key: COLUMN_KEYS.CHANNEL, |
| title: t('渠道'), |
| dataIndex: 'channel', |
| render: (text, record, index) => { |
| let isMultiKey = false; |
| let multiKeyIndex = -1; |
| let other = getLogOther(record.other); |
| if (other?.admin_info) { |
| let adminInfo = other.admin_info; |
| if (adminInfo?.is_multi_key) { |
| isMultiKey = true; |
| multiKeyIndex = adminInfo.multi_key_index; |
| } |
| } |
|
|
| return isAdminUser && |
| (record.type === 0 || record.type === 2 || record.type === 5) ? ( |
| <Space> |
| <Tooltip content={record.channel_name || t('未知渠道')}> |
| <span> |
| <Tag |
| color={colors[parseInt(text) % colors.length]} |
| shape='circle' |
| > |
| {text} |
| </Tag> |
| </span> |
| </Tooltip> |
| {isMultiKey && ( |
| <Tag color='white' shape='circle'> |
| {multiKeyIndex} |
| </Tag> |
| )} |
| </Space> |
| ) : null; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.USERNAME, |
| title: t('用户'), |
| dataIndex: 'username', |
| render: (text, record, index) => { |
| return isAdminUser ? ( |
| <div> |
| <Avatar |
| size='extra-small' |
| color={stringToColor(text)} |
| style={{ marginRight: 4 }} |
| onClick={(event) => { |
| event.stopPropagation(); |
| showUserInfoFunc(record.user_id); |
| }} |
| > |
| {typeof text === 'string' && text.slice(0, 1)} |
| </Avatar> |
| {text} |
| </div> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.TOKEN, |
| title: t('令牌'), |
| dataIndex: 'token_name', |
| render: (text, record, index) => { |
| return record.type === 0 || record.type === 2 || record.type === 5 ? ( |
| <div> |
| <Tag |
| color='grey' |
| shape='circle' |
| onClick={(event) => { |
| copyText(event, text); |
| }} |
| > |
| {' '} |
| {t(text)}{' '} |
| </Tag> |
| </div> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.GROUP, |
| title: t('分组'), |
| dataIndex: 'group', |
| render: (text, record, index) => { |
| if (record.type === 0 || record.type === 2 || record.type === 5) { |
| if (record.group) { |
| return <>{renderGroup(record.group)}</>; |
| } else { |
| let other = null; |
| try { |
| other = JSON.parse(record.other); |
| } catch (e) { |
| console.error( |
| `Failed to parse record.other: "${record.other}".`, |
| e, |
| ); |
| } |
| if (other === null) { |
| return <></>; |
| } |
| if (other.group !== undefined) { |
| return <>{renderGroup(other.group)}</>; |
| } else { |
| return <></>; |
| } |
| } |
| } else { |
| return <></>; |
| } |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.TYPE, |
| title: t('类型'), |
| dataIndex: 'type', |
| render: (text, record, index) => { |
| return <>{renderType(text, t)}</>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.MODEL, |
| title: t('模型'), |
| dataIndex: 'model_name', |
| render: (text, record, index) => { |
| return record.type === 0 || record.type === 2 || record.type === 5 ? ( |
| <>{renderModelName(record, copyText, t)}</> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.USE_TIME, |
| title: t('用时/首字'), |
| dataIndex: 'use_time', |
| render: (text, record, index) => { |
| if (!(record.type === 2 || record.type === 5)) { |
| return <></>; |
| } |
| if (record.is_stream) { |
| let other = getLogOther(record.other); |
| return ( |
| <> |
| <Space> |
| {renderUseTime(text, t)} |
| {renderFirstUseTime(other?.frt, t)} |
| {renderIsStream(record.is_stream, t)} |
| </Space> |
| </> |
| ); |
| } else { |
| return ( |
| <> |
| <Space> |
| {renderUseTime(text, t)} |
| {renderIsStream(record.is_stream, t)} |
| </Space> |
| </> |
| ); |
| } |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.PROMPT, |
| title: t('输入'), |
| dataIndex: 'prompt_tokens', |
| render: (text, record, index) => { |
| return record.type === 0 || record.type === 2 || record.type === 5 ? ( |
| <>{<span> {text} </span>}</> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.COMPLETION, |
| title: t('输出'), |
| dataIndex: 'completion_tokens', |
| render: (text, record, index) => { |
| return parseInt(text) > 0 && |
| (record.type === 0 || record.type === 2 || record.type === 5) ? ( |
| <>{<span> {text} </span>}</> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.COST, |
| title: t('花费'), |
| dataIndex: 'quota', |
| render: (text, record, index) => { |
| return record.type === 0 || record.type === 2 || record.type === 5 ? ( |
| <>{renderQuota(text, 6)}</> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.IP, |
| title: ( |
| <div className='flex items-center gap-1'> |
| {t('IP')} |
| <Tooltip |
| content={t( |
| '只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录', |
| )} |
| > |
| <IconHelpCircle className='text-gray-400 cursor-help' /> |
| </Tooltip> |
| </div> |
| ), |
| dataIndex: 'ip', |
| render: (text, record, index) => { |
| return (record.type === 2 || record.type === 5) && text ? ( |
| <Tooltip content={text}> |
| <span> |
| <Tag |
| color='orange' |
| shape='circle' |
| onClick={(event) => { |
| copyText(event, text); |
| }} |
| > |
| {text} |
| </Tag> |
| </span> |
| </Tooltip> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.RETRY, |
| title: t('重试'), |
| dataIndex: 'retry', |
| render: (text, record, index) => { |
| if (!(record.type === 2 || record.type === 5)) { |
| return <></>; |
| } |
| let content = t('渠道') + `:${record.channel}`; |
| if (record.other !== '') { |
| let other = JSON.parse(record.other); |
| if (other === null) { |
| return <></>; |
| } |
| if (other.admin_info !== undefined) { |
| if ( |
| other.admin_info.use_channel !== null && |
| other.admin_info.use_channel !== undefined && |
| other.admin_info.use_channel !== '' |
| ) { |
| let useChannel = other.admin_info.use_channel; |
| let useChannelStr = useChannel.join('->'); |
| content = t('渠道') + `:${useChannelStr}`; |
| } |
| } |
| } |
| return isAdminUser ? <div>{content}</div> : <></>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.DETAILS, |
| title: t('详情'), |
| dataIndex: 'content', |
| fixed: 'right', |
| render: (text, record, index) => { |
| let other = getLogOther(record.other); |
| if (other == null || record.type !== 2) { |
| return ( |
| <Typography.Paragraph |
| ellipsis={{ |
| rows: 2, |
| showTooltip: { |
| type: 'popover', |
| opts: { style: { width: 240 } }, |
| }, |
| }} |
| style={{ maxWidth: 240 }} |
| > |
| {text} |
| </Typography.Paragraph> |
| ); |
| } |
| let content = other?.claude |
| ? renderModelPriceSimple( |
| other.model_ratio, |
| other.model_price, |
| other.group_ratio, |
| other?.user_group_ratio, |
| other.cache_tokens || 0, |
| other.cache_ratio || 1.0, |
| other.cache_creation_tokens || 0, |
| other.cache_creation_ratio || 1.0, |
| other.cache_creation_tokens_5m || 0, |
| other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0, |
| other.cache_creation_tokens_1h || 0, |
| other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0, |
| false, |
| 1.0, |
| other?.is_system_prompt_overwritten, |
| 'claude', |
| ) |
| : renderModelPriceSimple( |
| other.model_ratio, |
| other.model_price, |
| other.group_ratio, |
| other?.user_group_ratio, |
| other.cache_tokens || 0, |
| other.cache_ratio || 1.0, |
| 0, |
| 1.0, |
| 0, |
| 1.0, |
| 0, |
| 1.0, |
| false, |
| 1.0, |
| other?.is_system_prompt_overwritten, |
| 'openai', |
| ); |
| return ( |
| <Typography.Paragraph |
| ellipsis={{ |
| rows: 3, |
| }} |
| style={{ maxWidth: 240, whiteSpace: 'pre-line' }} |
| > |
| {content} |
| </Typography.Paragraph> |
| ); |
| }, |
| }, |
| ]; |
| }; |
|
|