ChannelsTable.js 18 KB

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