ChannelsTable.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react';
  3. import { Link } from 'react-router-dom';
  4. import { API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
  5. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
  6. import {renderGroup, renderNumber, renderQuota} from '../helpers/render';
  7. function renderTimestamp(timestamp) {
  8. return (
  9. <>
  10. {timestamp2string(timestamp)}
  11. </>
  12. );
  13. }
  14. let type2label = undefined;
  15. function renderType(type) {
  16. if (!type2label) {
  17. type2label = new Map;
  18. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  19. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  20. }
  21. type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
  22. }
  23. return <Label basic color={type2label[type]?.color}>{type2label[type]?.text}</Label>;
  24. }
  25. function renderBalance(type, balance) {
  26. switch (type) {
  27. case 1: // OpenAI
  28. return <span>${balance.toFixed(2)}</span>;
  29. case 4: // CloseAI
  30. return <span>¥{balance.toFixed(2)}</span>;
  31. case 8: // 自定义
  32. return <span>${balance.toFixed(2)}</span>;
  33. case 5: // OpenAI-SB
  34. return <span>¥{(balance / 10000).toFixed(2)}</span>;
  35. case 10: // AI Proxy
  36. return <span>{renderNumber(balance)}</span>;
  37. case 12: // API2GPT
  38. return <span>¥{balance.toFixed(2)}</span>;
  39. case 13: // AIGC2D
  40. return <span>{renderNumber(balance)}</span>;
  41. default:
  42. return <span>不支持</span>;
  43. }
  44. }
  45. const ChannelsTable = () => {
  46. const [channels, setChannels] = useState([]);
  47. const [loading, setLoading] = useState(true);
  48. const [activePage, setActivePage] = useState(1);
  49. const [searchKeyword, setSearchKeyword] = useState('');
  50. const [searching, setSearching] = useState(false);
  51. const [updatingBalance, setUpdatingBalance] = useState(false);
  52. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  53. const [showPrompt, setShowPrompt] = useState(shouldShowPrompt("channel-test"));
  54. const loadChannels = async (startIdx) => {
  55. const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}`);
  56. const { success, message, data } = res.data;
  57. if (success) {
  58. if (startIdx === 0) {
  59. setChannels(data);
  60. } else {
  61. let newChannels = [...channels];
  62. newChannels.splice(startIdx * pageSize, data.length, ...data);
  63. setChannels(newChannels);
  64. }
  65. } else {
  66. showError(message);
  67. }
  68. setLoading(false);
  69. };
  70. const onPaginationChange = (e, { activePage }) => {
  71. (async () => {
  72. if (activePage === Math.ceil(channels.length / pageSize) + 1) {
  73. // In this case we have to load more data and then append them.
  74. await loadChannels(activePage - 1, pageSize);
  75. }
  76. setActivePage(activePage);
  77. })();
  78. };
  79. const setItemsPerPage = (e) => {
  80. console.log(e.target.value);
  81. //parseInt(e.target.value);
  82. setPageSize(parseInt(e.target.value));
  83. loadChannels(0);
  84. }
  85. const refresh = async () => {
  86. setLoading(true);
  87. await loadChannels(activePage - 1);
  88. };
  89. useEffect(() => {
  90. loadChannels(0)
  91. .then()
  92. .catch((reason) => {
  93. showError(reason);
  94. });
  95. }, []);
  96. const manageChannel = async (id, action, idx, value) => {
  97. let data = { id };
  98. let res;
  99. switch (action) {
  100. case 'delete':
  101. res = await API.delete(`/api/channel/${id}/`);
  102. break;
  103. case 'enable':
  104. data.status = 1;
  105. res = await API.put('/api/channel/', data);
  106. break;
  107. case 'disable':
  108. data.status = 2;
  109. res = await API.put('/api/channel/', data);
  110. break;
  111. case 'priority':
  112. if (value === '') {
  113. return;
  114. }
  115. data.priority = parseInt(value);
  116. res = await API.put('/api/channel/', data);
  117. break;
  118. case 'weight':
  119. if (value === '') {
  120. return;
  121. }
  122. data.weight = parseInt(value);
  123. if (data.weight < 0) {
  124. data.weight = 0;
  125. }
  126. res = await API.put('/api/channel/', data);
  127. break;
  128. }
  129. const { success, message } = res.data;
  130. if (success) {
  131. showSuccess('操作成功完成!');
  132. let channel = res.data.data;
  133. let newChannels = [...channels];
  134. let realIdx = (activePage - 1) * pageSize + idx;
  135. if (action === 'delete') {
  136. newChannels[realIdx].deleted = true;
  137. } else {
  138. newChannels[realIdx].status = channel.status;
  139. }
  140. setChannels(newChannels);
  141. } else {
  142. showError(message);
  143. }
  144. };
  145. const renderStatus = (status) => {
  146. switch (status) {
  147. case 1:
  148. return <Label basic color='green'>已启用</Label>;
  149. case 2:
  150. return (
  151. <Popup
  152. trigger={<Label basic color='red'>
  153. 已禁用
  154. </Label>}
  155. content='本渠道被手动禁用'
  156. basic
  157. />
  158. );
  159. case 3:
  160. return (
  161. <Popup
  162. trigger={<Label basic color='yellow'>
  163. 已禁用
  164. </Label>}
  165. content='本渠道被程序自动禁用'
  166. basic
  167. />
  168. );
  169. default:
  170. return (
  171. <Label basic color='grey'>
  172. 未知状态
  173. </Label>
  174. );
  175. }
  176. };
  177. const renderResponseTime = (responseTime) => {
  178. let time = responseTime / 1000;
  179. time = time.toFixed(2) + ' 秒';
  180. if (responseTime === 0) {
  181. return <Label basic color='grey'>未测试</Label>;
  182. } else if (responseTime <= 1000) {
  183. return <Label basic color='green'>{time}</Label>;
  184. } else if (responseTime <= 3000) {
  185. return <Label basic color='olive'>{time}</Label>;
  186. } else if (responseTime <= 5000) {
  187. return <Label basic color='yellow'>{time}</Label>;
  188. } else {
  189. return <Label basic color='red'>{time}</Label>;
  190. }
  191. };
  192. const searchChannels = async () => {
  193. if (searchKeyword === '') {
  194. // if keyword is blank, load files instead.
  195. await loadChannels(0);
  196. setActivePage(1);
  197. return;
  198. }
  199. setSearching(true);
  200. const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
  201. const { success, message, data } = res.data;
  202. if (success) {
  203. setChannels(data);
  204. setActivePage(1);
  205. } else {
  206. showError(message);
  207. }
  208. setSearching(false);
  209. };
  210. const testChannel = async (id, name, idx) => {
  211. const res = await API.get(`/api/channel/test/${id}/`);
  212. const { success, message, time } = res.data;
  213. if (success) {
  214. let newChannels = [...channels];
  215. let realIdx = (activePage - 1) * pageSize + idx;
  216. newChannels[realIdx].response_time = time * 1000;
  217. newChannels[realIdx].test_time = Date.now() / 1000;
  218. setChannels(newChannels);
  219. showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
  220. } else {
  221. showError(message);
  222. }
  223. };
  224. const testAllChannels = async () => {
  225. const res = await API.get(`/api/channel/test`);
  226. const { success, message } = res.data;
  227. if (success) {
  228. showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
  229. } else {
  230. showError(message);
  231. }
  232. };
  233. const deleteAllDisabledChannels = async () => {
  234. const res = await API.delete(`/api/channel/disabled`);
  235. const { success, message, data } = res.data;
  236. if (success) {
  237. showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
  238. await refresh();
  239. } else {
  240. showError(message);
  241. }
  242. };
  243. const updateChannelBalance = async (id, name, idx) => {
  244. const res = await API.get(`/api/channel/update_balance/${id}/`);
  245. const { success, message, balance } = res.data;
  246. if (success) {
  247. let newChannels = [...channels];
  248. let realIdx = (activePage - 1) * pageSize + idx;
  249. newChannels[realIdx].balance = balance;
  250. newChannels[realIdx].balance_updated_time = Date.now() / 1000;
  251. setChannels(newChannels);
  252. showInfo(`通道 ${name} 余额更新成功!`);
  253. } else {
  254. showError(message);
  255. }
  256. };
  257. const updateAllChannelsBalance = async () => {
  258. setUpdatingBalance(true);
  259. const res = await API.get(`/api/channel/update_balance`);
  260. const { success, message } = res.data;
  261. if (success) {
  262. showInfo('已更新完毕所有已启用通道余额!');
  263. } else {
  264. showError(message);
  265. }
  266. setUpdatingBalance(false);
  267. };
  268. const handleKeywordChange = async (e, { value }) => {
  269. setSearchKeyword(value.trim());
  270. };
  271. const sortChannel = (key) => {
  272. if (channels.length === 0) return;
  273. setLoading(true);
  274. let sortedChannels = [...channels];
  275. if (typeof sortedChannels[0][key] === 'string') {
  276. sortedChannels.sort((a, b) => {
  277. return ('' + a[key]).localeCompare(b[key]);
  278. });
  279. } else {
  280. sortedChannels.sort((a, b) => {
  281. if (a[key] === b[key]) return 0;
  282. if (a[key] > b[key]) return -1;
  283. if (a[key] < b[key]) return 1;
  284. });
  285. }
  286. if (sortedChannels[0].id === channels[0].id) {
  287. sortedChannels.reverse();
  288. }
  289. setChannels(sortedChannels);
  290. setLoading(false);
  291. };
  292. return (
  293. <>
  294. <Form onSubmit={searchChannels}>
  295. <Form.Input
  296. icon='search'
  297. fluid
  298. iconPosition='left'
  299. placeholder='搜索渠道的 ID,名称和密钥 ...'
  300. value={searchKeyword}
  301. loading={searching}
  302. onChange={handleKeywordChange}
  303. />
  304. </Form>
  305. {
  306. showPrompt && (
  307. <Message onDismiss={() => {
  308. setShowPrompt(false);
  309. setPromptShown("channel-test");
  310. }}>
  311. 当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo
  312. 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。
  313. 另外,OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
  314. </Message>
  315. )
  316. }
  317. <Table basic compact size='small'>
  318. <Table.Header>
  319. <Table.Row>
  320. <Table.HeaderCell
  321. style={{ cursor: 'pointer' }}
  322. onClick={() => {
  323. sortChannel('id');
  324. }}
  325. >
  326. ID
  327. </Table.HeaderCell>
  328. <Table.HeaderCell
  329. style={{ cursor: 'pointer' }}
  330. onClick={() => {
  331. sortChannel('name');
  332. }}
  333. >
  334. 名称
  335. </Table.HeaderCell>
  336. <Table.HeaderCell
  337. style={{ cursor: 'pointer' }}
  338. onClick={() => {
  339. sortChannel('group');
  340. }}
  341. width={1}
  342. >
  343. 分组
  344. </Table.HeaderCell>
  345. <Table.HeaderCell
  346. style={{ cursor: 'pointer' }}
  347. onClick={() => {
  348. sortChannel('type');
  349. }}
  350. width={2}
  351. >
  352. 类型
  353. </Table.HeaderCell>
  354. <Table.HeaderCell
  355. style={{ cursor: 'pointer' }}
  356. onClick={() => {
  357. sortChannel('status');
  358. }}
  359. width={2}
  360. >
  361. 状态
  362. </Table.HeaderCell>
  363. <Table.HeaderCell
  364. style={{ cursor: 'pointer' }}
  365. onClick={() => {
  366. sortChannel('response_time');
  367. }}
  368. >
  369. 响应时间
  370. </Table.HeaderCell>
  371. <Table.HeaderCell
  372. style={{ cursor: 'pointer' }}
  373. onClick={() => {
  374. sortChannel('used_quota');
  375. }}
  376. width={1}
  377. >
  378. 已使用
  379. </Table.HeaderCell>
  380. <Table.HeaderCell
  381. style={{ cursor: 'pointer' }}
  382. onClick={() => {
  383. sortChannel('balance');
  384. }}
  385. >
  386. 余额
  387. </Table.HeaderCell>
  388. <Table.HeaderCell
  389. style={{ cursor: 'pointer' }}
  390. onClick={() => {
  391. sortChannel('priority');
  392. }}
  393. >
  394. 优先级
  395. </Table.HeaderCell>
  396. <Table.HeaderCell>操作</Table.HeaderCell>
  397. </Table.Row>
  398. </Table.Header>
  399. <Table.Body>
  400. {channels
  401. .slice(
  402. (activePage - 1) * pageSize,
  403. activePage * pageSize
  404. )
  405. .map((channel, idx) => {
  406. if (channel.deleted) return <></>;
  407. return (
  408. <Table.Row key={channel.id}>
  409. <Table.Cell>{channel.id}</Table.Cell>
  410. <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
  411. <Table.Cell>{renderGroup(channel.group)}</Table.Cell>
  412. <Table.Cell>{renderType(channel.type)}</Table.Cell>
  413. <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
  414. <Table.Cell>
  415. <Popup
  416. content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
  417. key={channel.id}
  418. trigger={renderResponseTime(channel.response_time)}
  419. basic
  420. />
  421. </Table.Cell>
  422. <Table.Cell>{renderQuota(channel.used_quota)}</Table.Cell>
  423. <Table.Cell>
  424. <Popup
  425. trigger={<span onClick={() => {
  426. updateChannelBalance(channel.id, channel.name, idx);
  427. }} style={{ cursor: 'pointer' }}>
  428. {renderBalance(channel.type, channel.balance)}
  429. </span>}
  430. content='点击更新'
  431. basic
  432. />
  433. </Table.Cell>
  434. <Table.Cell>
  435. <Popup
  436. trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
  437. manageChannel(
  438. channel.id,
  439. 'priority',
  440. idx,
  441. event.target.value
  442. );
  443. }}>
  444. <input style={{ maxWidth: '60px' }} />
  445. </Input>}
  446. content='渠道选择优先级,越高越优先'
  447. basic
  448. />
  449. </Table.Cell>
  450. <Table.Cell>
  451. <div>
  452. <Button
  453. size={'small'}
  454. positive
  455. onClick={() => {
  456. testChannel(channel.id, channel.name, idx);
  457. }}
  458. >
  459. 测试
  460. </Button>
  461. <Popup
  462. trigger={
  463. <Button size='small' negative>
  464. 删除
  465. </Button>
  466. }
  467. on='click'
  468. flowing
  469. hoverable
  470. >
  471. <Button
  472. negative
  473. onClick={() => {
  474. manageChannel(channel.id, 'delete', idx);
  475. }}
  476. >
  477. 删除渠道 {channel.name}
  478. </Button>
  479. </Popup>
  480. <Button
  481. size={'small'}
  482. onClick={() => {
  483. manageChannel(
  484. channel.id,
  485. channel.status === 1 ? 'disable' : 'enable',
  486. idx
  487. );
  488. }}
  489. >
  490. {channel.status === 1 ? '禁用' : '启用'}
  491. </Button>
  492. <Button
  493. size={'small'}
  494. as={Link}
  495. to={'/channel/edit/' + channel.id}
  496. >
  497. 编辑
  498. </Button>
  499. </div>
  500. </Table.Cell>
  501. </Table.Row>
  502. );
  503. })}
  504. </Table.Body>
  505. <Table.Footer>
  506. <Table.Row>
  507. <Table.HeaderCell colSpan='10'>
  508. <Button size='small' as={Link} to='/channel/add' loading={loading}>
  509. 添加新的渠道
  510. </Button>
  511. <Button size='small' loading={loading} onClick={testAllChannels}>
  512. 测试所有已启用通道
  513. </Button>
  514. <Button size='small' onClick={updateAllChannelsBalance}
  515. loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
  516. <div style={{ float: 'right' }}>
  517. <div className="ui labeled input" style={{marginRight: '10px'}}>
  518. <div className="ui label">每页数量</div>
  519. <Input type="number" style={{width: '70px'}} defaultValue={ITEMS_PER_PAGE} onBlur={setItemsPerPage}></Input>
  520. </div>
  521. <Pagination
  522. activePage={activePage}
  523. onPageChange={onPaginationChange}
  524. size='small'
  525. siblingRange={1}
  526. totalPages={
  527. Math.ceil(channels.length / pageSize) +
  528. (channels.length % pageSize === 0 ? 1 : 0)
  529. }
  530. />
  531. </div>
  532. <Popup
  533. trigger={
  534. <Button size='small' loading={loading}>
  535. 删除禁用渠道
  536. </Button>
  537. }
  538. on='click'
  539. flowing
  540. hoverable
  541. >
  542. <Button size='small' loading={loading} negative onClick={deleteAllDisabledChannels}>
  543. 确认删除
  544. </Button>
  545. </Popup>
  546. <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
  547. </Table.HeaderCell>
  548. </Table.Row>
  549. </Table.Footer>
  550. </Table>
  551. </>
  552. );
  553. };
  554. export default ChannelsTable;