LogsTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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: 'content',
  290. render: (text, record, index) => {
  291. if (record.other === '') {
  292. return (
  293. <Paragraph
  294. ellipsis={{
  295. rows: 2,
  296. showTooltip: {
  297. type: 'popover',
  298. opts: { style: { width: 240 } },
  299. },
  300. }}
  301. style={{ maxWidth: 240 }}
  302. >
  303. {text}
  304. </Paragraph>
  305. );
  306. }
  307. let other = JSON.parse(record.other);
  308. let content = renderModelPrice(
  309. record.prompt_tokens,
  310. record.completion_tokens,
  311. other.model_ratio,
  312. other.model_price,
  313. other.completion_ratio,
  314. other.group_ratio,
  315. );
  316. return (
  317. <Tooltip content={content}>
  318. <Paragraph
  319. ellipsis={{
  320. rows: 2,
  321. }}
  322. style={{ maxWidth: 240 }}
  323. >
  324. {text}
  325. </Paragraph>
  326. </Tooltip>
  327. );
  328. },
  329. },
  330. ];
  331. const [logs, setLogs] = useState([]);
  332. const [showStat, setShowStat] = useState(false);
  333. const [loading, setLoading] = useState(false);
  334. const [loadingStat, setLoadingStat] = useState(false);
  335. const [activePage, setActivePage] = useState(1);
  336. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  337. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  338. const [searchKeyword, setSearchKeyword] = useState('');
  339. const [searching, setSearching] = useState(false);
  340. const [logType, setLogType] = useState(0);
  341. const isAdminUser = isAdmin();
  342. let now = new Date();
  343. // 初始化start_timestamp为前一天
  344. const [inputs, setInputs] = useState({
  345. username: '',
  346. token_name: '',
  347. model_name: '',
  348. start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
  349. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  350. channel: '',
  351. });
  352. const {
  353. username,
  354. token_name,
  355. model_name,
  356. start_timestamp,
  357. end_timestamp,
  358. channel,
  359. } = inputs;
  360. const [stat, setStat] = useState({
  361. quota: 0,
  362. token: 0,
  363. });
  364. const handleInputChange = (value, name) => {
  365. setInputs((inputs) => ({ ...inputs, [name]: value }));
  366. };
  367. const getLogSelfStat = async () => {
  368. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  369. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  370. let res = await API.get(
  371. `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
  372. );
  373. const { success, message, data } = res.data;
  374. if (success) {
  375. setStat(data);
  376. } else {
  377. showError(message);
  378. }
  379. };
  380. const getLogStat = async () => {
  381. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  382. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  383. let res = await API.get(
  384. `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`,
  385. );
  386. const { success, message, data } = res.data;
  387. if (success) {
  388. setStat(data);
  389. } else {
  390. showError(message);
  391. }
  392. };
  393. const handleEyeClick = async () => {
  394. setLoadingStat(true);
  395. if (isAdminUser) {
  396. await getLogStat();
  397. } else {
  398. await getLogSelfStat();
  399. }
  400. setShowStat(true);
  401. setLoadingStat(false);
  402. };
  403. const showUserInfo = async (userId) => {
  404. if (!isAdminUser) {
  405. return;
  406. }
  407. const res = await API.get(`/api/user/${userId}`);
  408. const { success, message, data } = res.data;
  409. if (success) {
  410. Modal.info({
  411. title: '用户信息',
  412. content: (
  413. <div style={{ padding: 12 }}>
  414. <p>用户名: {data.username}</p>
  415. <p>余额: {renderQuota(data.quota)}</p>
  416. <p>已用额度:{renderQuota(data.used_quota)}</p>
  417. <p>请求次数:{renderNumber(data.request_count)}</p>
  418. </div>
  419. ),
  420. centered: true,
  421. });
  422. } else {
  423. showError(message);
  424. }
  425. };
  426. const setLogsFormat = (logs) => {
  427. for (let i = 0; i < logs.length; i++) {
  428. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  429. logs[i].key = '' + logs[i].id;
  430. }
  431. // data.key = '' + data.id
  432. setLogs(logs);
  433. setLogCount(logs.length + ITEMS_PER_PAGE);
  434. // console.log(logCount);
  435. };
  436. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  437. setLoading(true);
  438. let url = '';
  439. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  440. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  441. if (isAdminUser) {
  442. 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}`;
  443. } else {
  444. 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}`;
  445. }
  446. const res = await API.get(url);
  447. const { success, message, data } = res.data;
  448. if (success) {
  449. if (startIdx === 0) {
  450. setLogsFormat(data);
  451. } else {
  452. let newLogs = [...logs];
  453. newLogs.splice(startIdx * pageSize, data.length, ...data);
  454. setLogsFormat(newLogs);
  455. }
  456. } else {
  457. showError(message);
  458. }
  459. setLoading(false);
  460. };
  461. const pageData = logs.slice(
  462. (activePage - 1) * pageSize,
  463. activePage * pageSize,
  464. );
  465. const handlePageChange = (page) => {
  466. setActivePage(page);
  467. if (page === Math.ceil(logs.length / pageSize) + 1) {
  468. // In this case we have to load more data and then append them.
  469. loadLogs(page - 1, pageSize, logType).then((r) => {});
  470. }
  471. };
  472. const handlePageSizeChange = async (size) => {
  473. localStorage.setItem('page-size', size + '');
  474. setPageSize(size);
  475. setActivePage(1);
  476. loadLogs(0, size)
  477. .then()
  478. .catch((reason) => {
  479. showError(reason);
  480. });
  481. };
  482. const refresh = async () => {
  483. // setLoading(true);
  484. setActivePage(1);
  485. await loadLogs(0, pageSize, logType);
  486. };
  487. const copyText = async (text) => {
  488. if (await copy(text)) {
  489. showSuccess('已复制:' + text);
  490. } else {
  491. // setSearchKeyword(text);
  492. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  493. }
  494. };
  495. useEffect(() => {
  496. // console.log('default effect')
  497. const localPageSize =
  498. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  499. setPageSize(localPageSize);
  500. loadLogs(0, localPageSize)
  501. .then()
  502. .catch((reason) => {
  503. showError(reason);
  504. });
  505. }, []);
  506. const searchLogs = async () => {
  507. if (searchKeyword === '') {
  508. // if keyword is blank, load files instead.
  509. await loadLogs(0, pageSize);
  510. setActivePage(1);
  511. return;
  512. }
  513. setSearching(true);
  514. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  515. const { success, message, data } = res.data;
  516. if (success) {
  517. setLogs(data);
  518. setActivePage(1);
  519. } else {
  520. showError(message);
  521. }
  522. setSearching(false);
  523. };
  524. return (
  525. <>
  526. <Layout>
  527. <Header>
  528. <Spin spinning={loadingStat}>
  529. <h3>
  530. 使用明细(总消耗额度:
  531. <span
  532. onClick={handleEyeClick}
  533. style={{
  534. cursor: 'pointer',
  535. color: 'gray',
  536. }}
  537. >
  538. {showStat ? renderQuota(stat.quota) : '点击查看'}
  539. </span>
  540. </h3>
  541. </Spin>
  542. </Header>
  543. <Form layout='horizontal' style={{ marginTop: 10 }}>
  544. <>
  545. <Form.Input
  546. field='token_name'
  547. label='令牌名称'
  548. style={{ width: 176 }}
  549. value={token_name}
  550. placeholder={'可选值'}
  551. name='token_name'
  552. onChange={(value) => handleInputChange(value, 'token_name')}
  553. />
  554. <Form.Input
  555. field='model_name'
  556. label='模型名称'
  557. style={{ width: 176 }}
  558. value={model_name}
  559. placeholder='可选值'
  560. name='model_name'
  561. onChange={(value) => handleInputChange(value, 'model_name')}
  562. />
  563. <Form.DatePicker
  564. field='start_timestamp'
  565. label='起始时间'
  566. style={{ width: 272 }}
  567. initValue={start_timestamp}
  568. value={start_timestamp}
  569. type='dateTime'
  570. name='start_timestamp'
  571. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  572. />
  573. <Form.DatePicker
  574. field='end_timestamp'
  575. fluid
  576. label='结束时间'
  577. style={{ width: 272 }}
  578. initValue={end_timestamp}
  579. value={end_timestamp}
  580. type='dateTime'
  581. name='end_timestamp'
  582. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  583. />
  584. {isAdminUser && (
  585. <>
  586. <Form.Input
  587. field='channel'
  588. label='渠道 ID'
  589. style={{ width: 176 }}
  590. value={channel}
  591. placeholder='可选值'
  592. name='channel'
  593. onChange={(value) => handleInputChange(value, 'channel')}
  594. />
  595. <Form.Input
  596. field='username'
  597. label='用户名称'
  598. style={{ width: 176 }}
  599. value={username}
  600. placeholder={'可选值'}
  601. name='username'
  602. onChange={(value) => handleInputChange(value, 'username')}
  603. />
  604. </>
  605. )}
  606. <Form.Section>
  607. <Button
  608. label='查询'
  609. type='primary'
  610. htmlType='submit'
  611. className='btn-margin-right'
  612. onClick={refresh}
  613. loading={loading}
  614. >
  615. 查询
  616. </Button>
  617. </Form.Section>
  618. </>
  619. </Form>
  620. <Table
  621. style={{ marginTop: 5 }}
  622. columns={columns}
  623. dataSource={pageData}
  624. pagination={{
  625. currentPage: activePage,
  626. pageSize: pageSize,
  627. total: logCount,
  628. pageSizeOpts: [10, 20, 50, 100],
  629. showSizeChanger: true,
  630. onPageSizeChange: (size) => {
  631. handlePageSizeChange(size).then();
  632. },
  633. onPageChange: handlePageChange,
  634. }}
  635. />
  636. <Select
  637. defaultValue='0'
  638. style={{ width: 120 }}
  639. onChange={(value) => {
  640. setLogType(parseInt(value));
  641. loadLogs(0, pageSize, parseInt(value));
  642. }}
  643. >
  644. <Select.Option value='0'>全部</Select.Option>
  645. <Select.Option value='1'>充值</Select.Option>
  646. <Select.Option value='2'>消费</Select.Option>
  647. <Select.Option value='3'>管理</Select.Option>
  648. <Select.Option value='4'>系统</Select.Option>
  649. </Select>
  650. </Layout>
  651. </>
  652. );
  653. };
  654. export default LogsTable;