LogsTable.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  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. const res = await API.get(url);
  517. const { success, message, data } = res.data;
  518. if (success) {
  519. if (startIdx === 0) {
  520. setLogsFormat(data);
  521. } else {
  522. let newLogs = [...logs];
  523. newLogs.splice(startIdx * pageSize, data.length, ...data);
  524. setLogsFormat(newLogs);
  525. }
  526. } else {
  527. showError(message);
  528. }
  529. setLoading(false);
  530. };
  531. const pageData = logs.slice(
  532. (activePage - 1) * pageSize,
  533. activePage * pageSize,
  534. );
  535. const handlePageChange = (page) => {
  536. setActivePage(page);
  537. if (page === Math.ceil(logs.length / pageSize) + 1) {
  538. // In this case we have to load more data and then append them.
  539. loadLogs(page - 1, pageSize, logType).then((r) => {});
  540. }
  541. };
  542. const handlePageSizeChange = async (size) => {
  543. localStorage.setItem('page-size', size + '');
  544. setPageSize(size);
  545. setActivePage(1);
  546. loadLogs(0, size)
  547. .then()
  548. .catch((reason) => {
  549. showError(reason);
  550. });
  551. };
  552. const refresh = async () => {
  553. // setLoading(true);
  554. setActivePage(1);
  555. await loadLogs(0, pageSize, logType);
  556. };
  557. const copyText = async (text) => {
  558. if (await copy(text)) {
  559. showSuccess('已复制:' + text);
  560. } else {
  561. // setSearchKeyword(text);
  562. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  563. }
  564. };
  565. useEffect(() => {
  566. // console.log('default effect')
  567. const localPageSize =
  568. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  569. setPageSize(localPageSize);
  570. loadLogs(0, localPageSize)
  571. .then()
  572. .catch((reason) => {
  573. showError(reason);
  574. });
  575. handleEyeClick();
  576. }, []);
  577. const searchLogs = async () => {
  578. if (searchKeyword === '') {
  579. // if keyword is blank, load files instead.
  580. await loadLogs(0, pageSize);
  581. setActivePage(1);
  582. return;
  583. }
  584. setSearching(true);
  585. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  586. const { success, message, data } = res.data;
  587. if (success) {
  588. setLogs(data);
  589. setActivePage(1);
  590. } else {
  591. showError(message);
  592. }
  593. setSearching(false);
  594. };
  595. return (
  596. <>
  597. <Layout>
  598. <Header>
  599. <Spin spinning={loadingStat}>
  600. <Space>
  601. <Tag color='green' size='large' style={{ padding: 15 }}>
  602. 总消耗额度: {renderQuota(stat.quota)}
  603. </Tag>
  604. <Tag color='blue' size='large' style={{ padding: 15 }}>
  605. RPM: {stat.rpm}
  606. </Tag>
  607. <Tag color='purple' size='large' style={{ padding: 15 }}>
  608. TPM: {stat.tpm}
  609. </Tag>
  610. </Space>
  611. </Spin>
  612. </Header>
  613. <Form layout='horizontal' style={{ marginTop: 10 }}>
  614. <>
  615. <Form.Input
  616. field='token_name'
  617. label='令牌名称'
  618. style={{ width: 176 }}
  619. value={token_name}
  620. placeholder={'可选值'}
  621. name='token_name'
  622. onChange={(value) => handleInputChange(value, 'token_name')}
  623. />
  624. <Form.Input
  625. field='model_name'
  626. label='模型名称'
  627. style={{ width: 176 }}
  628. value={model_name}
  629. placeholder='可选值'
  630. name='model_name'
  631. onChange={(value) => handleInputChange(value, 'model_name')}
  632. />
  633. <Form.DatePicker
  634. field='start_timestamp'
  635. label='起始时间'
  636. style={{ width: 272 }}
  637. initValue={start_timestamp}
  638. value={start_timestamp}
  639. type='dateTime'
  640. name='start_timestamp'
  641. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  642. />
  643. <Form.DatePicker
  644. field='end_timestamp'
  645. fluid
  646. label='结束时间'
  647. style={{ width: 272 }}
  648. initValue={end_timestamp}
  649. value={end_timestamp}
  650. type='dateTime'
  651. name='end_timestamp'
  652. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  653. />
  654. {isAdminUser && (
  655. <>
  656. <Form.Input
  657. field='channel'
  658. label='渠道 ID'
  659. style={{ width: 176 }}
  660. value={channel}
  661. placeholder='可选值'
  662. name='channel'
  663. onChange={(value) => handleInputChange(value, 'channel')}
  664. />
  665. <Form.Input
  666. field='username'
  667. label='用户名称'
  668. style={{ width: 176 }}
  669. value={username}
  670. placeholder={'可选值'}
  671. name='username'
  672. onChange={(value) => handleInputChange(value, 'username')}
  673. />
  674. </>
  675. )}
  676. <Button
  677. label='查询'
  678. type='primary'
  679. htmlType='submit'
  680. className='btn-margin-right'
  681. onClick={refresh}
  682. loading={loading}
  683. style={{ marginTop: 24 }}
  684. >
  685. 查询
  686. </Button>
  687. <Form.Section>
  688. </Form.Section>
  689. </>
  690. </Form>
  691. <Table
  692. style={{ marginTop: 5 }}
  693. columns={columns}
  694. dataSource={pageData}
  695. pagination={{
  696. currentPage: activePage,
  697. pageSize: pageSize,
  698. total: logCount,
  699. pageSizeOpts: [10, 20, 50, 100],
  700. showSizeChanger: true,
  701. onPageSizeChange: (size) => {
  702. handlePageSizeChange(size).then();
  703. },
  704. onPageChange: handlePageChange,
  705. }}
  706. />
  707. <Select
  708. defaultValue='0'
  709. style={{ width: 120 }}
  710. onChange={(value) => {
  711. setLogType(parseInt(value));
  712. loadLogs(0, pageSize, parseInt(value));
  713. handleEyeClick();
  714. }}
  715. >
  716. <Select.Option value='0'>全部</Select.Option>
  717. <Select.Option value='1'>充值</Select.Option>
  718. <Select.Option value='2'>消费</Select.Option>
  719. <Select.Option value='3'>管理</Select.Option>
  720. <Select.Option value='4'>系统</Select.Option>
  721. </Select>
  722. </Layout>
  723. </>
  724. );
  725. };
  726. export default LogsTable;