Explorar o código

Merge pull request #3399 from seefs001/refactor/codex-usage

Refactor/codex usage
Calcium-Ion hai 1 mes
pai
achega
16221f8279

+ 12 - 7
web/src/components/table/channels/ChannelsColumnDefs.jsx

@@ -538,19 +538,24 @@ export const getChannelsColumns = ({
                 </Tooltip>
                 <Tooltip
                   content={
-                    t('剩余额度') +
-                    ': ' +
-                    renderQuotaWithAmount(record.balance) +
-                    t(',点击更新')
+                    record.type === 57
+                      ? t('查看 Codex 帐号信息与用量')
+                      : t('剩余额度') +
+                        ': ' +
+                        renderQuotaWithAmount(record.balance) +
+                        t(',点击更新')
                   }
                 >
                   <Tag
-                    color='white'
-                    type='ghost'
+                    color={record.type === 57 ? 'light-blue' : 'white'}
+                    type={record.type === 57 ? 'light' : 'ghost'}
                     shape='circle'
+                    className={record.type === 57 ? 'cursor-pointer' : ''}
                     onClick={() => updateChannelBalance(record)}
                   >
-                    {renderQuotaWithAmount(record.balance)}
+                    {record.type === 57
+                      ? t('帐号信息')
+                      : renderQuotaWithAmount(record.balance)}
                   </Tag>
                 </Tooltip>
               </Space>

+ 188 - 48
web/src/components/table/channels/modals/CodexUsageModal.jsx

@@ -22,9 +22,11 @@ import {
   Modal,
   Button,
   Progress,
-  Tag,
   Typography,
   Spin,
+  Tag,
+  Descriptions,
+  Collapse,
 } from '@douyinfe/semi-ui';
 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 tt = typeof t === 'function' ? t : (v) => v;
   const hasWindowData =
@@ -181,50 +264,100 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
 
 const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
   const tt = typeof t === 'function' ? t : (v) => v;
+  const [showRawJson, setShowRawJson] = useState(false);
   const data = payload?.data ?? null;
   const rateLimit = data?.rate_limit ?? {};
   const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
-
-  const allowed = !!rateLimit?.allowed;
-  const limitReached = !!rateLimit?.limit_reached;
   const upstreamStatus = payload?.upstream_status;
-
-  const statusTag =
-    allowed && !limitReached ? (
-      <Tag color='green'>{tt('可用')}</Tag>
-    ) : (
-      <Tag color='red'>{tt('受限')}</Tag>
-    );
+  const accountType = data?.plan_type ?? rateLimit?.plan_type;
+  const accountTypeLabel = formatAccountTypeLabel(accountType, tt);
+  const accountTypeTagColor = getAccountTypeTagColor(accountType);
+  const statusTag = resolveUsageStatusTag(tt, rateLimit);
+  const userId = data?.user_id;
+  const email = data?.email;
+  const accountId = data?.account_id;
+  const errorMessage =
+    payload?.success === false ? getDisplayText(payload?.message) || tt('获取用量失败') : '';
 
   const rawText =
     typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
 
   return (
-    <div className='flex flex-col gap-3'>
-      <div className='flex flex-wrap items-center justify-between gap-2'>
-        <Text type='tertiary' size='small'>
+    <div className='flex flex-col gap-4'>
+      {errorMessage && (
+        <div className='rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'>
+          {errorMessage}
+        </div>
+      )}
+
+      <div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-3'>
+        <div className='flex flex-wrap items-start justify-between gap-2'>
+          <div className='min-w-0'>
+            <div className='text-xs font-medium text-semi-color-text-2'>
+              {tt('Codex 帐号')}
+            </div>
+            <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('刷新')}
+          </Button>
+        </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 || '-'})
-        </Text>
-        <div className='flex items-center gap-2'>
-          {statusTag}
-          <Button
-            size='small'
-            type='tertiary'
-            theme='borderless'
-            onClick={onRefresh}
-          >
-            {tt('刷新')}
-          </Button>
         </div>
       </div>
 
-      <div className='flex flex-wrap items-center justify-between gap-2'>
-        <Text type='tertiary' size='small'>
-          {tt('上游状态码:')}
-          {upstreamStatus ?? '-'}
-        </Text>
+      <div>
+        <div className='mb-2'>
+          <div className='text-sm font-semibold text-semi-color-text-0'>
+            {tt('额度窗口')}
+          </div>
+          <Text type='tertiary' size='small'>
+            {tt('用于观察当前帐号在 Codex 上游的限额使用情况')}
+          </Text>
+        </div>
       </div>
 
       <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 className='mb-1 flex items-center justify-between gap-2'>
-          <div className='text-sm font-medium'>{tt('原始 JSON')}</div>
-          <Button
-            size='small'
-            type='primary'
-            theme='outline'
-            onClick={() => onCopy?.(rawText)}
-            disabled={!rawText}
-          >
-            {tt('复制')}
-          </Button>
-        </div>
-        <pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
-          {rawText}
-        </pre>
-      </div>
+      <Collapse
+        activeKey={showRawJson ? ['raw-json'] : []}
+        onChange={(activeKey) => {
+          const keys = Array.isArray(activeKey) ? activeKey : [activeKey];
+          setShowRawJson(keys.includes('raw-json'));
+        }}
+      >
+        <Collapse.Panel header={tt('原始 JSON')} itemKey='raw-json'>
+          <div className='mb-2 flex justify-end'>
+            <Button
+              size='small'
+              type='primary'
+              theme='outline'
+              onClick={() => onCopy?.(rawText)}
+              disabled={!rawText}
+            >
+              {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>
   );
 };
@@ -351,7 +491,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
   const tt = typeof t === 'function' ? t : (v) => v;
 
   Modal.info({
-    title: tt('Codex 用量'),
+    title: tt('Codex 帐号与用量'),
     centered: true,
     width: 900,
     style: { maxWidth: '95vw' },