LogsTable.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. copy,
  5. isAdmin,
  6. showError,
  7. showSuccess,
  8. timestamp2string,
  9. } from '../helpers';
  10. import {
  11. Avatar,
  12. Button,
  13. Form,
  14. Layout,
  15. Modal,
  16. Select,
  17. Space,
  18. Spin,
  19. Table,
  20. Tag,
  21. Tooltip,
  22. } from '@douyinfe/semi-ui';
  23. import { ITEMS_PER_PAGE } from '../constants';
  24. import {
  25. renderModelPrice,
  26. renderNumber,
  27. renderQuota,
  28. stringToColor,
  29. } from '../helpers/render';
  30. import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
  31. const { Header } = Layout;
  32. function renderTimestamp(timestamp) {
  33. return <>{timestamp2string(timestamp)}</>;
  34. }
  35. const MODE_OPTIONS = [
  36. { key: 'all', text: '全部用户', value: 'all' },
  37. { key: 'self', text: '当前用户', value: 'self' },
  38. ];
  39. const colors = [
  40. 'amber',
  41. 'blue',
  42. 'cyan',
  43. 'green',
  44. 'grey',
  45. 'indigo',
  46. 'light-blue',
  47. 'lime',
  48. 'orange',
  49. 'pink',
  50. 'purple',
  51. 'red',
  52. 'teal',
  53. 'violet',
  54. 'yellow',
  55. ];
  56. function renderType(type) {
  57. switch (type) {
  58. case 1:
  59. return (
  60. <Tag color='cyan' size='large'>
  61. {' '}
  62. 充值{' '}
  63. </Tag>
  64. );
  65. case 2:
  66. return (
  67. <Tag color='lime' size='large'>
  68. {' '}
  69. 消费{' '}
  70. </Tag>
  71. );
  72. case 3:
  73. return (
  74. <Tag color='orange' size='large'>
  75. {' '}
  76. 管理{' '}
  77. </Tag>
  78. );
  79. case 4:
  80. return (
  81. <Tag color='purple' size='large'>
  82. {' '}
  83. 系统{' '}
  84. </Tag>
  85. );
  86. default:
  87. return (
  88. <Tag color='black' size='large'>
  89. {' '}
  90. 未知{' '}
  91. </Tag>
  92. );
  93. }
  94. }
  95. function renderIsStream(bool) {
  96. if (bool) {
  97. return (
  98. <Tag color='blue' size='large'>
  99. </Tag>
  100. );
  101. } else {
  102. return (
  103. <Tag color='purple' size='large'>
  104. 非流
  105. </Tag>
  106. );
  107. }
  108. }
  109. function renderUseTime(type) {
  110. const time = parseInt(type);
  111. if (time < 101) {
  112. return (
  113. <Tag color='green' size='large'>
  114. {' '}
  115. {time} s{' '}
  116. </Tag>
  117. );
  118. } else if (time < 300) {
  119. return (
  120. <Tag color='orange' size='large'>
  121. {' '}
  122. {time} s{' '}
  123. </Tag>
  124. );
  125. } else {
  126. return (
  127. <Tag color='red' size='large'>
  128. {' '}
  129. {time} s{' '}
  130. </Tag>
  131. );
  132. }
  133. }
  134. const LogsTable = () => {
  135. const columns = [
  136. {
  137. title: '时间',
  138. dataIndex: 'timestamp2string',
  139. },
  140. {
  141. title: '渠道',
  142. dataIndex: 'channel',
  143. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  144. render: (text, record, index) => {
  145. return isAdminUser ? (
  146. record.type === 0 || record.type === 2 ? (
  147. <div>
  148. {
  149. <Tag
  150. color={colors[parseInt(text) % colors.length]}
  151. size='large'
  152. >
  153. {' '}
  154. {text}{' '}
  155. </Tag>
  156. }
  157. </div>
  158. ) : (
  159. <></>
  160. )
  161. ) : (
  162. <></>
  163. );
  164. },
  165. },
  166. {
  167. title: '用户',
  168. dataIndex: 'username',
  169. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  170. render: (text, record, index) => {
  171. return isAdminUser ? (
  172. <div>
  173. <Avatar
  174. size='small'
  175. color={stringToColor(text)}
  176. style={{ marginRight: 4 }}
  177. onClick={() => showUserInfo(record.user_id)}
  178. >
  179. {typeof text === 'string' && text.slice(0, 1)}
  180. </Avatar>
  181. {text}
  182. </div>
  183. ) : (
  184. <></>
  185. );
  186. },
  187. },
  188. {
  189. title: '令牌',
  190. dataIndex: 'token_name',
  191. render: (text, record, index) => {
  192. return record.type === 0 || record.type === 2 ? (
  193. <div>
  194. <Tag
  195. color='grey'
  196. size='large'
  197. onClick={() => {
  198. copyText(text);
  199. }}
  200. >
  201. {' '}
  202. {text}{' '}
  203. </Tag>
  204. </div>
  205. ) : (
  206. <></>
  207. );
  208. },
  209. },
  210. {
  211. title: '类型',
  212. dataIndex: 'type',
  213. render: (text, record, index) => {
  214. return <div>{renderType(text)}</div>;
  215. },
  216. },
  217. {
  218. title: '模型',
  219. dataIndex: 'model_name',
  220. render: (text, record, index) => {
  221. return record.type === 0 || record.type === 2 ? (
  222. <div>
  223. <Tag
  224. color={stringToColor(text)}
  225. size='large'
  226. onClick={() => {
  227. copyText(text);
  228. }}
  229. >
  230. {' '}
  231. {text}{' '}
  232. </Tag>
  233. </div>
  234. ) : (
  235. <></>
  236. );
  237. },
  238. },
  239. {
  240. title: '用时',
  241. dataIndex: 'use_time',
  242. render: (text, record, index) => {
  243. return (
  244. <div>
  245. <Space>
  246. {renderUseTime(text)}
  247. {renderIsStream(record.is_stream)}
  248. </Space>
  249. </div>
  250. );
  251. },
  252. },
  253. {
  254. title: '提示',
  255. dataIndex: 'prompt_tokens',
  256. render: (text, record, index) => {
  257. return record.type === 0 || record.type === 2 ? (
  258. <div>{<span> {text} </span>}</div>
  259. ) : (
  260. <></>
  261. );
  262. },
  263. },
  264. {
  265. title: '补全',
  266. dataIndex: 'completion_tokens',
  267. render: (text, record, index) => {
  268. return parseInt(text) > 0 &&
  269. (record.type === 0 || record.type === 2) ? (
  270. <div>{<span> {text} </span>}</div>
  271. ) : (
  272. <></>
  273. );
  274. },
  275. },
  276. {
  277. title: '花费',
  278. dataIndex: 'quota',
  279. render: (text, record, index) => {
  280. return record.type === 0 || record.type === 2 ? (
  281. <div>{renderQuota(text, 6)}</div>
  282. ) : (
  283. <></>
  284. );
  285. },
  286. },
  287. {
  288. title: '重试',
  289. dataIndex: 'retry',
  290. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  291. render: (text, record, index) => {
  292. let content = '渠道:' + record.channel;
  293. if (record.other !== '') {
  294. let other = JSON.parse(record.other);
  295. if (other === null) {
  296. return <></>
  297. }
  298. if (other.admin_info !== undefined) {
  299. if (
  300. other.admin_info.use_channel !== null &&
  301. other.admin_info.use_channel !== undefined &&
  302. other.admin_info.use_channel !== ''
  303. ) {
  304. // channel id array
  305. let useChannel = other.admin_info.use_channel;
  306. let useChannelStr = useChannel.join('->');
  307. content = `渠道:${useChannelStr}`;
  308. }
  309. }
  310. }
  311. return isAdminUser ? <div>{content}</div> : <></>;
  312. },
  313. },
  314. {
  315. title: '详情',
  316. dataIndex: 'content',
  317. render: (text, record, index) => {
  318. let other = JSON.parse(record.other);
  319. if (other == null) {
  320. return (
  321. <Paragraph
  322. ellipsis={{
  323. rows: 2,
  324. showTooltip: {
  325. type: 'popover',
  326. opts: { style: { width: 240 } },
  327. },
  328. }}
  329. style={{ maxWidth: 240 }}
  330. >
  331. {text}
  332. </Paragraph>
  333. );
  334. }
  335. let content = renderModelPrice(
  336. record.prompt_tokens,
  337. record.completion_tokens,
  338. other.model_ratio,
  339. other.model_price,
  340. other.completion_ratio,
  341. other.group_ratio,
  342. );
  343. return (
  344. <Tooltip content={content}>
  345. <Paragraph
  346. ellipsis={{
  347. rows: 2,
  348. }}
  349. style={{ maxWidth: 240 }}
  350. >
  351. {text}
  352. </Paragraph>
  353. </Tooltip>
  354. );
  355. },
  356. },
  357. ];
  358. const [logs, setLogs] = useState([]);
  359. const [showStat, setShowStat] = useState(false);
  360. const [loading, setLoading] = useState(false);
  361. const [loadingStat, setLoadingStat] = useState(false);
  362. const [activePage, setActivePage] = useState(1);
  363. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  364. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  365. const [searchKeyword, setSearchKeyword] = useState('');
  366. const [searching, setSearching] = useState(false);
  367. const [logType, setLogType] = useState(0);
  368. const isAdminUser = isAdmin();
  369. let now = new Date();
  370. // 初始化start_timestamp为前一天
  371. const [inputs, setInputs] = useState({
  372. username: '',
  373. token_name: '',
  374. model_name: '',
  375. start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
  376. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  377. channel: '',
  378. });
  379. const {
  380. username,
  381. token_name,
  382. model_name,
  383. start_timestamp,
  384. end_timestamp,
  385. channel,
  386. } = inputs;
  387. const [stat, setStat] = useState({
  388. quota: 0,
  389. token: 0,
  390. });
  391. const handleInputChange = (value, name) => {
  392. setInputs((inputs) => ({ ...inputs, [name]: value }));
  393. };
  394. const getLogSelfStat = async () => {
  395. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  396. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  397. let res = await API.get(
  398. `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
  399. );
  400. const { success, message, data } = res.data;
  401. if (success) {
  402. setStat(data);
  403. } else {
  404. showError(message);
  405. }
  406. };
  407. const getLogStat = async () => {
  408. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  409. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  410. let res = await API.get(
  411. `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`,
  412. );
  413. const { success, message, data } = res.data;
  414. if (success) {
  415. setStat(data);
  416. } else {
  417. showError(message);
  418. }
  419. };
  420. const handleEyeClick = async () => {
  421. setLoadingStat(true);
  422. if (isAdminUser) {
  423. await getLogStat();
  424. } else {
  425. await getLogSelfStat();
  426. }
  427. setShowStat(true);
  428. setLoadingStat(false);
  429. };
  430. const showUserInfo = async (userId) => {
  431. if (!isAdminUser) {
  432. return;
  433. }
  434. const res = await API.get(`/api/user/${userId}`);
  435. const { success, message, data } = res.data;
  436. if (success) {
  437. Modal.info({
  438. title: '用户信息',
  439. content: (
  440. <div style={{ padding: 12 }}>
  441. <p>用户名: {data.username}</p>
  442. <p>余额: {renderQuota(data.quota)}</p>
  443. <p>已用额度:{renderQuota(data.used_quota)}</p>
  444. <p>请求次数:{renderNumber(data.request_count)}</p>
  445. </div>
  446. ),
  447. centered: true,
  448. });
  449. } else {
  450. showError(message);
  451. }
  452. };
  453. const setLogsFormat = (logs) => {
  454. for (let i = 0; i < logs.length; i++) {
  455. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  456. logs[i].key = '' + logs[i].id;
  457. }
  458. // data.key = '' + data.id
  459. setLogs(logs);
  460. setLogCount(logs.length + ITEMS_PER_PAGE);
  461. // console.log(logCount);
  462. };
  463. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  464. setLoading(true);
  465. let url = '';
  466. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  467. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  468. if (isAdminUser) {
  469. url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
  470. } else {
  471. url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  472. }
  473. const res = await API.get(url);
  474. const { success, message, data } = res.data;
  475. if (success) {
  476. if (startIdx === 0) {
  477. setLogsFormat(data);
  478. } else {
  479. let newLogs = [...logs];
  480. newLogs.splice(startIdx * pageSize, data.length, ...data);
  481. setLogsFormat(newLogs);
  482. }
  483. } else {
  484. showError(message);
  485. }
  486. setLoading(false);
  487. };
  488. const pageData = logs.slice(
  489. (activePage - 1) * pageSize,
  490. activePage * pageSize,
  491. );
  492. const handlePageChange = (page) => {
  493. setActivePage(page);
  494. if (page === Math.ceil(logs.length / pageSize) + 1) {
  495. // In this case we have to load more data and then append them.
  496. loadLogs(page - 1, pageSize, logType).then((r) => {});
  497. }
  498. };
  499. const handlePageSizeChange = async (size) => {
  500. localStorage.setItem('page-size', size + '');
  501. setPageSize(size);
  502. setActivePage(1);
  503. loadLogs(0, size)
  504. .then()
  505. .catch((reason) => {
  506. showError(reason);
  507. });
  508. };
  509. const refresh = async () => {
  510. // setLoading(true);
  511. setActivePage(1);
  512. await loadLogs(0, pageSize, logType);
  513. };
  514. const copyText = async (text) => {
  515. if (await copy(text)) {
  516. showSuccess('已复制:' + text);
  517. } else {
  518. // setSearchKeyword(text);
  519. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  520. }
  521. };
  522. useEffect(() => {
  523. // console.log('default effect')
  524. const localPageSize =
  525. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  526. setPageSize(localPageSize);
  527. loadLogs(0, localPageSize)
  528. .then()
  529. .catch((reason) => {
  530. showError(reason);
  531. });
  532. }, []);
  533. const searchLogs = async () => {
  534. if (searchKeyword === '') {
  535. // if keyword is blank, load files instead.
  536. await loadLogs(0, pageSize);
  537. setActivePage(1);
  538. return;
  539. }
  540. setSearching(true);
  541. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  542. const { success, message, data } = res.data;
  543. if (success) {
  544. setLogs(data);
  545. setActivePage(1);
  546. } else {
  547. showError(message);
  548. }
  549. setSearching(false);
  550. };
  551. return (
  552. <>
  553. <Layout>
  554. <Header>
  555. <Spin spinning={loadingStat}>
  556. <h3>
  557. 使用明细(总消耗额度:
  558. <span
  559. onClick={handleEyeClick}
  560. style={{
  561. cursor: 'pointer',
  562. color: 'gray',
  563. }}
  564. >
  565. {showStat ? renderQuota(stat.quota) : '点击查看'}
  566. </span>
  567. </h3>
  568. </Spin>
  569. </Header>
  570. <Form layout='horizontal' style={{ marginTop: 10 }}>
  571. <>
  572. <Form.Input
  573. field='token_name'
  574. label='令牌名称'
  575. style={{ width: 176 }}
  576. value={token_name}
  577. placeholder={'可选值'}
  578. name='token_name'
  579. onChange={(value) => handleInputChange(value, 'token_name')}
  580. />
  581. <Form.Input
  582. field='model_name'
  583. label='模型名称'
  584. style={{ width: 176 }}
  585. value={model_name}
  586. placeholder='可选值'
  587. name='model_name'
  588. onChange={(value) => handleInputChange(value, 'model_name')}
  589. />
  590. <Form.DatePicker
  591. field='start_timestamp'
  592. label='起始时间'
  593. style={{ width: 272 }}
  594. initValue={start_timestamp}
  595. value={start_timestamp}
  596. type='dateTime'
  597. name='start_timestamp'
  598. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  599. />
  600. <Form.DatePicker
  601. field='end_timestamp'
  602. fluid
  603. label='结束时间'
  604. style={{ width: 272 }}
  605. initValue={end_timestamp}
  606. value={end_timestamp}
  607. type='dateTime'
  608. name='end_timestamp'
  609. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  610. />
  611. {isAdminUser && (
  612. <>
  613. <Form.Input
  614. field='channel'
  615. label='渠道 ID'
  616. style={{ width: 176 }}
  617. value={channel}
  618. placeholder='可选值'
  619. name='channel'
  620. onChange={(value) => handleInputChange(value, 'channel')}
  621. />
  622. <Form.Input
  623. field='username'
  624. label='用户名称'
  625. style={{ width: 176 }}
  626. value={username}
  627. placeholder={'可选值'}
  628. name='username'
  629. onChange={(value) => handleInputChange(value, 'username')}
  630. />
  631. </>
  632. )}
  633. <Form.Section>
  634. <Button
  635. label='查询'
  636. type='primary'
  637. htmlType='submit'
  638. className='btn-margin-right'
  639. onClick={refresh}
  640. loading={loading}
  641. >
  642. 查询
  643. </Button>
  644. </Form.Section>
  645. </>
  646. </Form>
  647. <Table
  648. style={{ marginTop: 5 }}
  649. columns={columns}
  650. dataSource={pageData}
  651. pagination={{
  652. currentPage: activePage,
  653. pageSize: pageSize,
  654. total: logCount,
  655. pageSizeOpts: [10, 20, 50, 100],
  656. showSizeChanger: true,
  657. onPageSizeChange: (size) => {
  658. handlePageSizeChange(size).then();
  659. },
  660. onPageChange: handlePageChange,
  661. }}
  662. />
  663. <Select
  664. defaultValue='0'
  665. style={{ width: 120 }}
  666. onChange={(value) => {
  667. setLogType(parseInt(value));
  668. loadLogs(0, pageSize, parseInt(value));
  669. }}
  670. >
  671. <Select.Option value='0'>全部</Select.Option>
  672. <Select.Option value='1'>充值</Select.Option>
  673. <Select.Option value='2'>消费</Select.Option>
  674. <Select.Option value='3'>管理</Select.Option>
  675. <Select.Option value='4'>系统</Select.Option>
  676. </Select>
  677. </Layout>
  678. </>
  679. );
  680. };
  681. export default LogsTable;