mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 21:57:27 +00:00
feat: codex channel (#2652)
* feat: codex channel * feat: codex channel * feat: codex oauth flow * feat: codex refresh cred * feat: codex usage * fix: codex err message detail * fix: codex setting ui * feat: codex refresh cred task * fix: import err * fix: codex store must be false * fix: chat -> responses tool call * fix: chat -> responses tool call
This commit is contained in:
151
web/src/components/table/channels/modals/CodexOAuthModal.jsx
Normal file
151
web/src/components/table/channels/modals/CodexOAuthModal.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
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 support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Button, Space, Typography, Input, Banner } from '@douyinfe/semi-ui';
|
||||
import { API, copy, showError, showSuccess } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [authorizeUrl, setAuthorizeUrl] = useState('');
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const startOAuth = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/channel/codex/oauth/start', {}, { skipErrorHandler: true });
|
||||
if (!res?.data?.success) {
|
||||
console.error('Codex OAuth start failed:', res?.data?.message);
|
||||
throw new Error(t('启动授权失败'));
|
||||
}
|
||||
const url = res?.data?.data?.authorize_url || '';
|
||||
if (!url) {
|
||||
console.error('Codex OAuth start response missing authorize_url:', res?.data);
|
||||
throw new Error(t('响应缺少授权链接'));
|
||||
}
|
||||
setAuthorizeUrl(url);
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
showSuccess(t('已打开授权页面'));
|
||||
} catch (error) {
|
||||
showError(error?.message || t('启动授权失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const completeOAuth = async () => {
|
||||
if (!input || !input.trim()) {
|
||||
showError(t('请先粘贴回调 URL'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post(
|
||||
'/api/channel/codex/oauth/complete',
|
||||
{ input },
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
if (!res?.data?.success) {
|
||||
console.error('Codex OAuth complete failed:', res?.data?.message);
|
||||
throw new Error(t('授权失败'));
|
||||
}
|
||||
|
||||
const key = res?.data?.data?.key || '';
|
||||
if (!key) {
|
||||
console.error('Codex OAuth complete response missing key:', res?.data);
|
||||
throw new Error(t('响应缺少凭据'));
|
||||
}
|
||||
|
||||
onSuccess && onSuccess(key);
|
||||
showSuccess(t('已生成授权凭据'));
|
||||
onCancel && onCancel();
|
||||
} catch (error) {
|
||||
showError(error?.message || t('授权失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
setAuthorizeUrl('');
|
||||
setInput('');
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('Codex 授权')}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
closeOnEsc
|
||||
width={720}
|
||||
footer={
|
||||
<Space>
|
||||
<Button theme='borderless' onClick={onCancel} disabled={loading}>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button theme='solid' type='primary' onClick={completeOAuth} loading={loading}>
|
||||
{t('生成并填入')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space vertical spacing='tight' style={{ width: '100%' }}>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t(
|
||||
'1) 点击「打开授权页面」完成登录;2) 浏览器会跳转到 localhost(页面打不开也没关系);3) 复制地址栏完整 URL 粘贴到下方;4) 点击「生成并填入」。',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Space wrap>
|
||||
<Button type='primary' onClick={startOAuth} loading={loading}>
|
||||
{t('打开授权页面')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='outline'
|
||||
disabled={!authorizeUrl || loading}
|
||||
onClick={() => copy(authorizeUrl)}
|
||||
>
|
||||
{t('复制授权链接')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(value) => setInput(value)}
|
||||
placeholder={t('请粘贴完整回调 URL(包含 code 与 state)')}
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。')}
|
||||
</Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodexOAuthModal;
|
||||
190
web/src/components/table/channels/modals/CodexUsageModal.jsx
Normal file
190
web/src/components/table/channels/modals/CodexUsageModal.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
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 support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal, Button, Progress, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const clampPercent = (value) => {
|
||||
const v = Number(value);
|
||||
if (!Number.isFinite(v)) return 0;
|
||||
return Math.max(0, Math.min(100, v));
|
||||
};
|
||||
|
||||
const pickStrokeColor = (percent) => {
|
||||
const p = clampPercent(percent);
|
||||
if (p >= 95) return '#ef4444';
|
||||
if (p >= 80) return '#f59e0b';
|
||||
return '#3b82f6';
|
||||
};
|
||||
|
||||
const formatDurationSeconds = (seconds, t) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const s = Number(seconds);
|
||||
if (!Number.isFinite(s) || s <= 0) return '-';
|
||||
const total = Math.floor(s);
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
const secs = total % 60;
|
||||
if (hours > 0) return `${hours}${tt('小时')} ${minutes}${tt('分钟')}`;
|
||||
if (minutes > 0) return `${minutes}${tt('分钟')} ${secs}${tt('秒')}`;
|
||||
return `${secs}${tt('秒')}`;
|
||||
};
|
||||
|
||||
const formatUnixSeconds = (unixSeconds) => {
|
||||
const v = Number(unixSeconds);
|
||||
if (!Number.isFinite(v) || v <= 0) return '-';
|
||||
try {
|
||||
return new Date(v * 1000).toLocaleString();
|
||||
} catch (error) {
|
||||
return String(unixSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const percent = clampPercent(windowData?.used_percent ?? 0);
|
||||
const resetAt = windowData?.reset_at;
|
||||
const resetAfterSeconds = windowData?.reset_after_seconds;
|
||||
const limitWindowSeconds = windowData?.limit_window_seconds;
|
||||
|
||||
return (
|
||||
<div className='rounded-lg border border-semi-color-border bg-semi-color-bg-0 p-3'>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<div className='font-medium'>{title}</div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('重置时间:')}
|
||||
{formatUnixSeconds(resetAt)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
<Progress
|
||||
percent={percent}
|
||||
stroke={pickStrokeColor(percent)}
|
||||
showInfo={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-1 flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
|
||||
<div>
|
||||
{tt('已使用:')}
|
||||
{percent}%
|
||||
</div>
|
||||
<div>
|
||||
{tt('距离重置:')}
|
||||
{formatDurationSeconds(resetAfterSeconds, tt)}
|
||||
</div>
|
||||
<div>
|
||||
{tt('窗口:')}
|
||||
{formatDurationSeconds(limitWindowSeconds, tt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const data = payload?.data ?? null;
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
|
||||
const primary = rateLimit?.primary_window ?? null;
|
||||
const secondary = rateLimit?.secondary_window ?? null;
|
||||
|
||||
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 rawText =
|
||||
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
||||
|
||||
Modal.info({
|
||||
title: (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{tt('Codex 用量')}</span>
|
||||
{statusTag}
|
||||
</div>
|
||||
),
|
||||
centered: true,
|
||||
width: 900,
|
||||
style: { maxWidth: '95vw' },
|
||||
content: (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
),
|
||||
footer: (
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button type='primary' theme='solid' onClick={() => Modal.destroyAll()}>
|
||||
{tt('关闭')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
} from '../../../../helpers';
|
||||
import ModelSelectModal from './ModelSelectModal';
|
||||
import OllamaModelModal from './OllamaModelModal';
|
||||
import CodexOAuthModal from './CodexOAuthModal';
|
||||
import JSONEditor from '../../../common/ui/JSONEditor';
|
||||
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
|
||||
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
||||
@@ -114,6 +115,8 @@ function type2secretPrompt(type) {
|
||||
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
||||
case 51:
|
||||
return '按照如下格式输入: AccessKey|SecretAccessKey';
|
||||
case 57:
|
||||
return '请输入 JSON 格式的 OAuth 凭据(必须包含 access_token 和 account_id)';
|
||||
default:
|
||||
return '请输入渠道对应的鉴权密钥';
|
||||
}
|
||||
@@ -212,6 +215,9 @@ const EditChannelModal = (props) => {
|
||||
}, [inputs.model_mapping]);
|
||||
const [isIonetChannel, setIsIonetChannel] = useState(false);
|
||||
const [ionetMetadata, setIonetMetadata] = useState(null);
|
||||
const [codexOAuthModalVisible, setCodexOAuthModalVisible] = useState(false);
|
||||
const [codexCredentialRefreshing, setCodexCredentialRefreshing] =
|
||||
useState(false);
|
||||
|
||||
// 密钥显示状态
|
||||
const [keyDisplayState, setKeyDisplayState] = useState({
|
||||
@@ -499,6 +505,18 @@ const EditChannelModal = (props) => {
|
||||
|
||||
// 重置手动输入模式状态
|
||||
setUseManualInput(false);
|
||||
|
||||
if (value === 57) {
|
||||
setBatch(false);
|
||||
setMultiToSingle(false);
|
||||
setMultiKeyMode('random');
|
||||
setVertexKeys([]);
|
||||
setVertexFileList([]);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('vertex_files', []);
|
||||
}
|
||||
setInputs((prev) => ({ ...prev, vertex_files: [] }));
|
||||
}
|
||||
}
|
||||
//setAutoBan
|
||||
};
|
||||
@@ -822,6 +840,32 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCodexOAuthGenerated = (key) => {
|
||||
handleInputChange('key', key);
|
||||
formatJsonField('key');
|
||||
};
|
||||
|
||||
const handleRefreshCodexCredential = async () => {
|
||||
if (!isEdit) return;
|
||||
|
||||
setCodexCredentialRefreshing(true);
|
||||
try {
|
||||
const res = await API.post(
|
||||
`/api/channel/${channelId}/codex/refresh`,
|
||||
{},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
if (!res?.data?.success) {
|
||||
throw new Error(res?.data?.message || 'Failed to refresh credential');
|
||||
}
|
||||
showSuccess(t('凭证已刷新'));
|
||||
} catch (error) {
|
||||
showError(error.message || t('刷新失败'));
|
||||
} finally {
|
||||
setCodexCredentialRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputs.type !== 45) {
|
||||
doubaoApiClickCountRef.current = 0;
|
||||
@@ -1070,6 +1114,47 @@ const EditChannelModal = (props) => {
|
||||
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
||||
let localInputs = { ...formValues };
|
||||
|
||||
if (localInputs.type === 57) {
|
||||
if (batch) {
|
||||
showInfo(t('Codex 渠道不支持批量创建'));
|
||||
return;
|
||||
}
|
||||
|
||||
const rawKey = (localInputs.key || '').trim();
|
||||
if (!isEdit && rawKey === '') {
|
||||
showInfo(t('请输入密钥!'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawKey !== '') {
|
||||
if (!verifyJSON(rawKey)) {
|
||||
showInfo(t('密钥必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(rawKey);
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
showInfo(t('密钥必须是 JSON 对象'));
|
||||
return;
|
||||
}
|
||||
const accessToken = String(parsed.access_token || '').trim();
|
||||
const accountId = String(parsed.account_id || '').trim();
|
||||
if (!accessToken) {
|
||||
showInfo(t('密钥 JSON 必须包含 access_token'));
|
||||
return;
|
||||
}
|
||||
if (!accountId) {
|
||||
showInfo(t('密钥 JSON 必须包含 account_id'));
|
||||
return;
|
||||
}
|
||||
localInputs.key = JSON.stringify(parsed);
|
||||
} catch (error) {
|
||||
showInfo(t('密钥必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localInputs.type === 41) {
|
||||
const keyType = localInputs.vertex_key_type || 'json';
|
||||
if (keyType === 'api_key') {
|
||||
@@ -1401,7 +1486,7 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchAllowed = !isEdit || isMultiKeyChannel;
|
||||
const batchAllowed = (!isEdit || isMultiKeyChannel) && inputs.type !== 57;
|
||||
const batchExtra = batchAllowed ? (
|
||||
<Space>
|
||||
{!isEdit && (
|
||||
@@ -1884,8 +1969,94 @@ const EditChannelModal = (props) => {
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 &&
|
||||
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||
{inputs.type === 57 ? (
|
||||
<>
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={
|
||||
isEdit
|
||||
? t('密钥(编辑模式下,保存的密钥不会显示)')
|
||||
: t('密钥')
|
||||
}
|
||||
placeholder={t(
|
||||
'请输入 JSON 格式的 OAuth 凭据,例如:\n{\n "access_token": "...",\n "account_id": "..." \n}',
|
||||
)}
|
||||
rules={
|
||||
isEdit
|
||||
? []
|
||||
: [{ required: true, message: t('请输入密钥') }]
|
||||
}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
disabled={isIonetLocked}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t(
|
||||
'仅支持 JSON 对象,必须包含 access_token 与 account_id',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Space wrap spacing='tight'>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() =>
|
||||
setCodexOAuthModalVisible(true)
|
||||
}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('Codex 授权')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleRefreshCodexCredential}
|
||||
loading={codexCredentialRefreshing}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('刷新凭证')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => formatJsonField('key')}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleShow2FAModal}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
autosize
|
||||
showClear
|
||||
/>
|
||||
|
||||
<CodexOAuthModal
|
||||
visible={codexOAuthModalVisible}
|
||||
onCancel={() => setCodexOAuthModalVisible(false)}
|
||||
onSuccess={handleCodexOAuthGenerated}
|
||||
/>
|
||||
</>
|
||||
) : inputs.type === 41 &&
|
||||
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||
<>
|
||||
{!batch && (
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
|
||||
@@ -184,6 +184,11 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'blue',
|
||||
label: 'Replicate',
|
||||
},
|
||||
{
|
||||
value: 57,
|
||||
color: 'blue',
|
||||
label: 'Codex (OpenAI OAuth)',
|
||||
},
|
||||
];
|
||||
|
||||
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
@@ -301,6 +301,7 @@ export function getChannelIcon(channelType) {
|
||||
switch (channelType) {
|
||||
case 1: // OpenAI
|
||||
case 3: // Azure OpenAI
|
||||
case 57: // Codex
|
||||
return <OpenAI size={iconSize} />;
|
||||
case 2: // Midjourney Proxy
|
||||
case 5: // Midjourney Proxy Plus
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { useIsMobile } from '../common/useIsMobile';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { Modal, Button } from '@douyinfe/semi-ui';
|
||||
import { openCodexUsageModal } from '../../components/table/channels/modals/CodexUsageModal';
|
||||
|
||||
export const useChannelsData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -745,6 +746,32 @@ export const useChannelsData = () => {
|
||||
};
|
||||
|
||||
const updateChannelBalance = async (record) => {
|
||||
if (record?.type === 57) {
|
||||
try {
|
||||
const res = await API.get(`/api/channel/${record.id}/codex/usage`, {
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
if (!res?.data?.success) {
|
||||
console.error('Codex usage fetch failed:', res?.data?.message);
|
||||
showError(t('获取用量失败'));
|
||||
}
|
||||
openCodexUsageModal({
|
||||
t,
|
||||
record,
|
||||
payload: res?.data,
|
||||
onCopy: async (text) => {
|
||||
const ok = await copy(text);
|
||||
if (ok) showSuccess(t('已复制'));
|
||||
else showError(t('复制失败'));
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Codex usage fetch error:', error);
|
||||
showError(t('获取用量失败'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await API.get(`/api/channel/update_balance/${record.id}/`);
|
||||
const { success, message, balance } = res.data;
|
||||
if (success) {
|
||||
|
||||
Reference in New Issue
Block a user