| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- /*
- 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 [email protected]
- */
- import React from 'react';
- import {
- Avatar,
- Space,
- Tag,
- Tooltip,
- Popover,
- Typography
- } from '@douyinfe/semi-ui';
- import {
- timestamp2string,
- renderGroup,
- renderQuota,
- stringToColor,
- getLogOther,
- renderModelTag,
- renderClaudeLogContent,
- renderClaudeModelPriceSimple,
- 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',
- ];
- // Render functions
- 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('未知渠道')}>
- <Tag
- color={colors[parseInt(text) % colors.length]}
- shape='circle'
- >
- {text}
- </Tag>
- </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}>
- <Tag
- color='orange'
- shape='circle'
- onClick={(event) => {
- copyText(event, text);
- }}
- >
- {text}
- </Tag>
- </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
- ? renderClaudeModelPriceSimple(
- 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,
- )
- : renderModelPriceSimple(
- other.model_ratio,
- other.model_price,
- other.group_ratio,
- other?.user_group_ratio,
- other.cache_tokens || 0,
- other.cache_ratio || 1.0,
- );
- return (
- <Typography.Paragraph
- ellipsis={{
- rows: 2,
- }}
- style={{ maxWidth: 240 }}
- >
- {content}
- </Typography.Paragraph>
- );
- },
- },
- ];
- };
|