| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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> |
| | ); |
| | }, |
| | }, |
| | ]; |
| | }; |
| |
|