LogsTable.js 20 KB

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