LogsTable.js 19 KB

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