Merge pull request #3399 from seefs001/refactor/codex-usage
Refactor/codex usage
This commit is contained in:
@@ -538,19 +538,24 @@ export const getChannelsColumns = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
t('剩余额度') +
|
record.type === 57
|
||||||
': ' +
|
? t('查看 Codex 帐号信息与用量')
|
||||||
renderQuotaWithAmount(record.balance) +
|
: t('剩余额度') +
|
||||||
t(',点击更新')
|
': ' +
|
||||||
|
renderQuotaWithAmount(record.balance) +
|
||||||
|
t(',点击更新')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tag
|
<Tag
|
||||||
color='white'
|
color={record.type === 57 ? 'light-blue' : 'white'}
|
||||||
type='ghost'
|
type={record.type === 57 ? 'light' : 'ghost'}
|
||||||
shape='circle'
|
shape='circle'
|
||||||
|
className={record.type === 57 ? 'cursor-pointer' : ''}
|
||||||
onClick={() => updateChannelBalance(record)}
|
onClick={() => updateChannelBalance(record)}
|
||||||
>
|
>
|
||||||
{renderQuotaWithAmount(record.balance)}
|
{record.type === 57
|
||||||
|
? t('帐号信息')
|
||||||
|
: renderQuotaWithAmount(record.balance)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
Progress,
|
Progress,
|
||||||
Tag,
|
|
||||||
Typography,
|
Typography,
|
||||||
Spin,
|
Spin,
|
||||||
|
Tag,
|
||||||
|
Descriptions,
|
||||||
|
Collapse,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { API, showError } from '../../../../helpers';
|
import { API, showError } from '../../../../helpers';
|
||||||
|
|
||||||
@@ -128,6 +130,87 @@ const formatUnixSeconds = (unixSeconds) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDisplayText = (value) => {
|
||||||
|
if (value == null) return '';
|
||||||
|
return String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAccountTypeLabel = (value, t) => {
|
||||||
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
|
const normalized = normalizePlanType(value);
|
||||||
|
switch (normalized) {
|
||||||
|
case 'free':
|
||||||
|
return 'Free';
|
||||||
|
case 'plus':
|
||||||
|
return 'Plus';
|
||||||
|
case 'pro':
|
||||||
|
return 'Pro';
|
||||||
|
case 'team':
|
||||||
|
return 'Team';
|
||||||
|
case 'enterprise':
|
||||||
|
return 'Enterprise';
|
||||||
|
default:
|
||||||
|
return getDisplayText(value) || tt('未识别');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountTypeTagColor = (value) => {
|
||||||
|
const normalized = normalizePlanType(value);
|
||||||
|
switch (normalized) {
|
||||||
|
case 'enterprise':
|
||||||
|
return 'green';
|
||||||
|
case 'team':
|
||||||
|
return 'cyan';
|
||||||
|
case 'pro':
|
||||||
|
return 'blue';
|
||||||
|
case 'plus':
|
||||||
|
return 'violet';
|
||||||
|
case 'free':
|
||||||
|
return 'amber';
|
||||||
|
default:
|
||||||
|
return 'grey';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveUsageStatusTag = (t, rateLimit) => {
|
||||||
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
|
if (!rateLimit || Object.keys(rateLimit).length === 0) {
|
||||||
|
return <Tag color='grey'>{tt('待确认')}</Tag>;
|
||||||
|
}
|
||||||
|
if (rateLimit?.allowed && !rateLimit?.limit_reached) {
|
||||||
|
return <Tag color='green'>{tt('可用')}</Tag>;
|
||||||
|
}
|
||||||
|
return <Tag color='red'>{tt('受限')}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountInfoValue = ({ t, value, onCopy, monospace = false }) => {
|
||||||
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
|
const text = getDisplayText(value);
|
||||||
|
const hasValue = text !== '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-w-0 items-start justify-between gap-2'>
|
||||||
|
<div
|
||||||
|
className={`min-w-0 flex-1 break-all text-xs leading-5 text-semi-color-text-1 ${
|
||||||
|
monospace ? 'font-mono' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{hasValue ? text : '-'}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
theme='borderless'
|
||||||
|
className='shrink-0 px-1 text-xs'
|
||||||
|
disabled={!hasValue}
|
||||||
|
onClick={() => onCopy?.(text)}
|
||||||
|
>
|
||||||
|
{tt('复制')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const RateLimitWindowCard = ({ t, title, windowData }) => {
|
const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||||
const tt = typeof t === 'function' ? t : (v) => v;
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
const hasWindowData =
|
const hasWindowData =
|
||||||
@@ -181,50 +264,100 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
|||||||
|
|
||||||
const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||||
const tt = typeof t === 'function' ? t : (v) => v;
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
|
const [showRawJson, setShowRawJson] = useState(false);
|
||||||
const data = payload?.data ?? null;
|
const data = payload?.data ?? null;
|
||||||
const rateLimit = data?.rate_limit ?? {};
|
const rateLimit = data?.rate_limit ?? {};
|
||||||
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
|
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
|
||||||
|
|
||||||
const allowed = !!rateLimit?.allowed;
|
|
||||||
const limitReached = !!rateLimit?.limit_reached;
|
|
||||||
const upstreamStatus = payload?.upstream_status;
|
const upstreamStatus = payload?.upstream_status;
|
||||||
|
const accountType = data?.plan_type ?? rateLimit?.plan_type;
|
||||||
const statusTag =
|
const accountTypeLabel = formatAccountTypeLabel(accountType, tt);
|
||||||
allowed && !limitReached ? (
|
const accountTypeTagColor = getAccountTypeTagColor(accountType);
|
||||||
<Tag color='green'>{tt('可用')}</Tag>
|
const statusTag = resolveUsageStatusTag(tt, rateLimit);
|
||||||
) : (
|
const userId = data?.user_id;
|
||||||
<Tag color='red'>{tt('受限')}</Tag>
|
const email = data?.email;
|
||||||
);
|
const accountId = data?.account_id;
|
||||||
|
const errorMessage =
|
||||||
|
payload?.success === false ? getDisplayText(payload?.message) || tt('获取用量失败') : '';
|
||||||
|
|
||||||
const rawText =
|
const rawText =
|
||||||
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
{errorMessage && (
|
||||||
<Text type='tertiary' size='small'>
|
<div className='rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'>
|
||||||
{tt('渠道:')}
|
{errorMessage}
|
||||||
{record?.name || '-'} ({tt('编号:')}
|
</div>
|
||||||
{record?.id || '-'})
|
)}
|
||||||
</Text>
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-3'>
|
||||||
{statusTag}
|
<div className='flex flex-wrap items-start justify-between gap-2'>
|
||||||
<Button
|
<div className='min-w-0'>
|
||||||
size='small'
|
<div className='text-xs font-medium text-semi-color-text-2'>
|
||||||
type='tertiary'
|
{tt('Codex 帐号')}
|
||||||
theme='borderless'
|
</div>
|
||||||
onClick={onRefresh}
|
<div className='mt-2 flex flex-wrap items-center gap-2'>
|
||||||
>
|
<Tag
|
||||||
|
color={accountTypeTagColor}
|
||||||
|
type='light'
|
||||||
|
shape='circle'
|
||||||
|
size='large'
|
||||||
|
className='font-semibold'
|
||||||
|
>
|
||||||
|
{accountTypeLabel}
|
||||||
|
</Tag>
|
||||||
|
{statusTag}
|
||||||
|
<Tag color='grey' type='light' shape='circle'>
|
||||||
|
{tt('上游状态码:')}
|
||||||
|
{upstreamStatus ?? '-'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size='small' type='tertiary' theme='outline' onClick={onRefresh}>
|
||||||
{tt('刷新')}
|
{tt('刷新')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-2 rounded-lg bg-semi-color-fill-0 px-3 py-2'>
|
||||||
|
<Descriptions>
|
||||||
|
<Descriptions.Item itemKey='User ID'>
|
||||||
|
<AccountInfoValue
|
||||||
|
t={tt}
|
||||||
|
value={userId}
|
||||||
|
onCopy={onCopy}
|
||||||
|
monospace={true}
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey={tt('邮箱')}>
|
||||||
|
<AccountInfoValue t={tt} value={email} onCopy={onCopy} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='Account ID'>
|
||||||
|
<AccountInfoValue
|
||||||
|
t={tt}
|
||||||
|
value={accountId}
|
||||||
|
onCopy={onCopy}
|
||||||
|
monospace={true}
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-2 text-xs text-semi-color-text-2'>
|
||||||
|
{tt('渠道:')}
|
||||||
|
{record?.name || '-'} ({tt('编号:')}
|
||||||
|
{record?.id || '-'})
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
<div>
|
||||||
<Text type='tertiary' size='small'>
|
<div className='mb-2'>
|
||||||
{tt('上游状态码:')}
|
<div className='text-sm font-semibold text-semi-color-text-0'>
|
||||||
{upstreamStatus ?? '-'}
|
{tt('额度窗口')}
|
||||||
</Text>
|
</div>
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{tt('用于观察当前帐号在 Codex 上游的限额使用情况')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||||
@@ -240,23 +373,30 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Collapse
|
||||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
activeKey={showRawJson ? ['raw-json'] : []}
|
||||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
onChange={(activeKey) => {
|
||||||
<Button
|
const keys = Array.isArray(activeKey) ? activeKey : [activeKey];
|
||||||
size='small'
|
setShowRawJson(keys.includes('raw-json'));
|
||||||
type='primary'
|
}}
|
||||||
theme='outline'
|
>
|
||||||
onClick={() => onCopy?.(rawText)}
|
<Collapse.Panel header={tt('原始 JSON')} itemKey='raw-json'>
|
||||||
disabled={!rawText}
|
<div className='mb-2 flex justify-end'>
|
||||||
>
|
<Button
|
||||||
{tt('复制')}
|
size='small'
|
||||||
</Button>
|
type='primary'
|
||||||
</div>
|
theme='outline'
|
||||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
onClick={() => onCopy?.(rawText)}
|
||||||
{rawText}
|
disabled={!rawText}
|
||||||
</pre>
|
>
|
||||||
</div>
|
{tt('复制')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<pre className='max-h-[50vh] overflow-y-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||||
|
{rawText}
|
||||||
|
</pre>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -351,7 +491,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
|||||||
const tt = typeof t === 'function' ? t : (v) => v;
|
const tt = typeof t === 'function' ? t : (v) => v;
|
||||||
|
|
||||||
Modal.info({
|
Modal.info({
|
||||||
title: tt('Codex 用量'),
|
title: tt('Codex 帐号与用量'),
|
||||||
centered: true,
|
centered: true,
|
||||||
width: 900,
|
width: 900,
|
||||||
style: { maxWidth: '95vw' },
|
style: { maxWidth: '95vw' },
|
||||||
|
|||||||
Reference in New Issue
Block a user