CodexOAuthModal.jsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import {
  18. Modal,
  19. Button,
  20. Space,
  21. Typography,
  22. Input,
  23. Banner,
  24. } from '@douyinfe/semi-ui';
  25. import { API, copy, showError, showSuccess } from '../../../../helpers';
  26. const { Text } = Typography;
  27. const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
  28. const { t } = useTranslation();
  29. const [loading, setLoading] = useState(false);
  30. const [authorizeUrl, setAuthorizeUrl] = useState('');
  31. const [input, setInput] = useState('');
  32. const startOAuth = async () => {
  33. setLoading(true);
  34. try {
  35. const res = await API.post(
  36. '/api/channel/codex/oauth/start',
  37. {},
  38. { skipErrorHandler: true },
  39. );
  40. if (!res?.data?.success) {
  41. console.error('Codex OAuth start failed:', res?.data?.message);
  42. throw new Error(t('启动授权失败'));
  43. }
  44. const url = res?.data?.data?.authorize_url || '';
  45. if (!url) {
  46. console.error(
  47. 'Codex OAuth start response missing authorize_url:',
  48. res?.data,
  49. );
  50. throw new Error(t('响应缺少授权链接'));
  51. }
  52. setAuthorizeUrl(url);
  53. window.open(url, '_blank', 'noopener,noreferrer');
  54. showSuccess(t('已打开授权页面'));
  55. } catch (error) {
  56. showError(error?.message || t('启动授权失败'));
  57. } finally {
  58. setLoading(false);
  59. }
  60. };
  61. const completeOAuth = async () => {
  62. if (!input || !input.trim()) {
  63. showError(t('请先粘贴回调 URL'));
  64. return;
  65. }
  66. setLoading(true);
  67. try {
  68. const res = await API.post(
  69. '/api/channel/codex/oauth/complete',
  70. { input },
  71. { skipErrorHandler: true },
  72. );
  73. if (!res?.data?.success) {
  74. console.error('Codex OAuth complete failed:', res?.data?.message);
  75. throw new Error(t('授权失败'));
  76. }
  77. const key = res?.data?.data?.key || '';
  78. if (!key) {
  79. console.error('Codex OAuth complete response missing key:', res?.data);
  80. throw new Error(t('响应缺少凭据'));
  81. }
  82. onSuccess && onSuccess(key);
  83. showSuccess(t('已生成授权凭据'));
  84. onCancel && onCancel();
  85. } catch (error) {
  86. showError(error?.message || t('授权失败'));
  87. } finally {
  88. setLoading(false);
  89. }
  90. };
  91. useEffect(() => {
  92. if (!visible) return;
  93. setAuthorizeUrl('');
  94. setInput('');
  95. }, [visible]);
  96. return (
  97. <Modal
  98. title={t('Codex 授权')}
  99. visible={visible}
  100. onCancel={onCancel}
  101. maskClosable={false}
  102. closeOnEsc
  103. width={720}
  104. footer={
  105. <Space>
  106. <Button theme='borderless' onClick={onCancel} disabled={loading}>
  107. {t('取消')}
  108. </Button>
  109. <Button
  110. theme='solid'
  111. type='primary'
  112. onClick={completeOAuth}
  113. loading={loading}
  114. >
  115. {t('生成并填入')}
  116. </Button>
  117. </Space>
  118. }
  119. >
  120. <Space vertical spacing='tight' style={{ width: '100%' }}>
  121. <Banner
  122. type='info'
  123. description={t(
  124. '1) 点击「打开授权页面」完成登录;2) 浏览器会跳转到 localhost(页面打不开也没关系);3) 复制地址栏完整 URL 粘贴到下方;4) 点击「生成并填入」。',
  125. )}
  126. />
  127. <Space wrap>
  128. <Button type='primary' onClick={startOAuth} loading={loading}>
  129. {t('打开授权页面')}
  130. </Button>
  131. <Button
  132. theme='outline'
  133. disabled={!authorizeUrl || loading}
  134. onClick={() => copy(authorizeUrl)}
  135. >
  136. {t('复制授权链接')}
  137. </Button>
  138. </Space>
  139. <Input
  140. value={input}
  141. onChange={(value) => setInput(value)}
  142. placeholder={t('请粘贴完整回调 URL(包含 code 与 state)')}
  143. showClear
  144. />
  145. <Text type='tertiary' size='small'>
  146. {t(
  147. '说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。',
  148. )}
  149. </Text>
  150. </Space>
  151. </Modal>
  152. );
  153. };
  154. export default CodexOAuthModal;