LogsTable.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. API,
  5. copy,
  6. getTodayStartTimestamp,
  7. isAdmin,
  8. showError,
  9. showSuccess,
  10. timestamp2string,
  11. } from '../helpers';
  12. import {
  13. Avatar,
  14. Button,
  15. Descriptions,
  16. Form,
  17. Layout,
  18. Modal,
  19. Popover,
  20. Select,
  21. Space,
  22. Spin,
  23. Table,
  24. Tag,
  25. Tooltip,
  26. Checkbox,
  27. } from '@douyinfe/semi-ui';
  28. import { ITEMS_PER_PAGE } from '../constants';
  29. import {
  30. renderAudioModelPrice,
  31. renderClaudeLogContent,
  32. renderClaudeModelPrice,
  33. renderClaudeModelPriceSimple,
  34. renderGroup,
  35. renderLogContent,
  36. renderModelPrice,
  37. renderModelPriceSimple,
  38. renderNumber,
  39. renderQuota,
  40. stringToColor,
  41. } from '../helpers/render';
  42. import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
  43. import { getLogOther } from '../helpers/other.js';
  44. import { StyleContext } from '../context/Style/index.js';
  45. import { IconInherit, IconRefresh, IconSetting } from '@douyinfe/semi-icons';
  46. const { Header } = Layout;
  47. function renderTimestamp(timestamp) {
  48. return <>{timestamp2string(timestamp)}</>;
  49. }
  50. const MODE_OPTIONS = [
  51. { key: 'all', text: 'all', value: 'all' },
  52. { key: 'self', text: 'current user', value: 'self' },
  53. ];
  54. const colors = [
  55. 'amber',
  56. 'blue',
  57. 'cyan',
  58. 'green',
  59. 'grey',
  60. 'indigo',
  61. 'light-blue',
  62. 'lime',
  63. 'orange',
  64. 'pink',
  65. 'purple',
  66. 'red',
  67. 'teal',
  68. 'violet',
  69. 'yellow',
  70. ];
  71. const LogsTable = () => {
  72. const { t } = useTranslation();
  73. function renderType(type) {
  74. switch (type) {
  75. case 1:
  76. return (
  77. <Tag color='cyan' size='large'>
  78. {t('充值')}
  79. </Tag>
  80. );
  81. case 2:
  82. return (
  83. <Tag color='lime' size='large'>
  84. {t('消费')}
  85. </Tag>
  86. );
  87. case 3:
  88. return (
  89. <Tag color='orange' size='large'>
  90. {t('管理')}
  91. </Tag>
  92. );
  93. case 4:
  94. return (
  95. <Tag color='purple' size='large'>
  96. {t('系统')}
  97. </Tag>
  98. );
  99. case 5:
  100. return (
  101. <Tag color='red' size='large'>
  102. {t('错误')}
  103. </Tag>
  104. );
  105. default:
  106. return (
  107. <Tag color='grey' size='large'>
  108. {t('未知')}
  109. </Tag>
  110. );
  111. }
  112. }
  113. function renderIsStream(bool) {
  114. if (bool) {
  115. return (
  116. <Tag color='blue' size='large'>
  117. {t('流')}
  118. </Tag>
  119. );
  120. } else {
  121. return (
  122. <Tag color='purple' size='large'>
  123. {t('非流')}
  124. </Tag>
  125. );
  126. }
  127. }
  128. function renderUseTime(type) {
  129. const time = parseInt(type);
  130. if (time < 101) {
  131. return (
  132. <Tag color='green' size='large'>
  133. {' '}
  134. {time} s{' '}
  135. </Tag>
  136. );
  137. } else if (time < 300) {
  138. return (
  139. <Tag color='orange' size='large'>
  140. {' '}
  141. {time} s{' '}
  142. </Tag>
  143. );
  144. } else {
  145. return (
  146. <Tag color='red' size='large'>
  147. {' '}
  148. {time} s{' '}
  149. </Tag>
  150. );
  151. }
  152. }
  153. function renderFirstUseTime(type) {
  154. let time = parseFloat(type) / 1000.0;
  155. time = time.toFixed(1);
  156. if (time < 3) {
  157. return (
  158. <Tag color='green' size='large'>
  159. {' '}
  160. {time} s{' '}
  161. </Tag>
  162. );
  163. } else if (time < 10) {
  164. return (
  165. <Tag color='orange' size='large'>
  166. {' '}
  167. {time} s{' '}
  168. </Tag>
  169. );
  170. } else {
  171. return (
  172. <Tag color='red' size='large'>
  173. {' '}
  174. {time} s{' '}
  175. </Tag>
  176. );
  177. }
  178. }
  179. function renderModelName(record) {
  180. let other = getLogOther(record.other);
  181. let modelMapped =
  182. other?.is_model_mapped &&
  183. other?.upstream_model_name &&
  184. other?.upstream_model_name !== '';
  185. if (!modelMapped) {
  186. return (
  187. <Tag
  188. color={stringToColor(record.model_name)}
  189. size='large'
  190. onClick={(event) => {
  191. copyText(event, record.model_name).then((r) => {});
  192. }}
  193. >
  194. {' '}
  195. {record.model_name}{' '}
  196. </Tag>
  197. );
  198. } else {
  199. return (
  200. <>
  201. <Space vertical align={'start'}>
  202. <Popover
  203. content={
  204. <div style={{ padding: 10 }}>
  205. <Space vertical align={'start'}>
  206. <Tag
  207. color={stringToColor(record.model_name)}
  208. size='large'
  209. onClick={(event) => {
  210. copyText(event, record.model_name).then((r) => {});
  211. }}
  212. >
  213. {t('请求并计费模型')} {record.model_name}{' '}
  214. </Tag>
  215. <Tag
  216. color={stringToColor(other.upstream_model_name)}
  217. size='large'
  218. onClick={(event) => {
  219. copyText(event, other.upstream_model_name).then(
  220. (r) => {},
  221. );
  222. }}
  223. >
  224. {t('实际模型')} {other.upstream_model_name}{' '}
  225. </Tag>
  226. </Space>
  227. </div>
  228. }
  229. >
  230. <Tag
  231. color={stringToColor(record.model_name)}
  232. size='large'
  233. onClick={(event) => {
  234. copyText(event, record.model_name).then((r) => {});
  235. }}
  236. suffixIcon={
  237. <IconRefresh
  238. style={{ width: '0.8em', height: '0.8em', opacity: 0.6 }}
  239. />
  240. }
  241. >
  242. {' '}
  243. {record.model_name}{' '}
  244. </Tag>
  245. </Popover>
  246. {/*<Tooltip content={t('实际模型')}>*/}
  247. {/* <Tag*/}
  248. {/* color={stringToColor(other.upstream_model_name)}*/}
  249. {/* size='large'*/}
  250. {/* onClick={(event) => {*/}
  251. {/* copyText(event, other.upstream_model_name).then(r => {});*/}
  252. {/* }}*/}
  253. {/* >*/}
  254. {/* {' '}{other.upstream_model_name}{' '}*/}
  255. {/* </Tag>*/}
  256. {/*</Tooltip>*/}
  257. </Space>
  258. </>
  259. );
  260. }
  261. }
  262. // Define column keys for selection
  263. const COLUMN_KEYS = {
  264. TIME: 'time',
  265. CHANNEL: 'channel',
  266. USERNAME: 'username',
  267. TOKEN: 'token',
  268. GROUP: 'group',
  269. TYPE: 'type',
  270. MODEL: 'model',
  271. USE_TIME: 'use_time',
  272. PROMPT: 'prompt',
  273. COMPLETION: 'completion',
  274. COST: 'cost',
  275. RETRY: 'retry',
  276. DETAILS: 'details',
  277. };
  278. // State for column visibility
  279. const [visibleColumns, setVisibleColumns] = useState({});
  280. const [showColumnSelector, setShowColumnSelector] = useState(false);
  281. // Load saved column preferences from localStorage
  282. useEffect(() => {
  283. const savedColumns = localStorage.getItem('logs-table-columns');
  284. if (savedColumns) {
  285. try {
  286. const parsed = JSON.parse(savedColumns);
  287. // Make sure all columns are accounted for
  288. const defaults = getDefaultColumnVisibility();
  289. const merged = { ...defaults, ...parsed };
  290. setVisibleColumns(merged);
  291. } catch (e) {
  292. console.error('Failed to parse saved column preferences', e);
  293. initDefaultColumns();
  294. }
  295. } else {
  296. initDefaultColumns();
  297. }
  298. }, []);
  299. // Get default column visibility based on user role
  300. const getDefaultColumnVisibility = () => {
  301. return {
  302. [COLUMN_KEYS.TIME]: true,
  303. [COLUMN_KEYS.CHANNEL]: isAdminUser,
  304. [COLUMN_KEYS.USERNAME]: isAdminUser,
  305. [COLUMN_KEYS.TOKEN]: true,
  306. [COLUMN_KEYS.GROUP]: true,
  307. [COLUMN_KEYS.TYPE]: true,
  308. [COLUMN_KEYS.MODEL]: true,
  309. [COLUMN_KEYS.USE_TIME]: true,
  310. [COLUMN_KEYS.PROMPT]: true,
  311. [COLUMN_KEYS.COMPLETION]: true,
  312. [COLUMN_KEYS.COST]: true,
  313. [COLUMN_KEYS.RETRY]: isAdminUser,
  314. [COLUMN_KEYS.DETAILS]: true,
  315. };
  316. };
  317. // Initialize default column visibility
  318. const initDefaultColumns = () => {
  319. const defaults = getDefaultColumnVisibility();
  320. setVisibleColumns(defaults);
  321. localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
  322. };
  323. // Handle column visibility change
  324. const handleColumnVisibilityChange = (columnKey, checked) => {
  325. const updatedColumns = { ...visibleColumns, [columnKey]: checked };
  326. setVisibleColumns(updatedColumns);
  327. };
  328. // Handle "Select All" checkbox
  329. const handleSelectAll = (checked) => {
  330. const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
  331. const updatedColumns = {};
  332. allKeys.forEach((key) => {
  333. // For admin-only columns, only enable them if user is admin
  334. if (
  335. (key === COLUMN_KEYS.CHANNEL ||
  336. key === COLUMN_KEYS.USERNAME ||
  337. key === COLUMN_KEYS.RETRY) &&
  338. !isAdminUser
  339. ) {
  340. updatedColumns[key] = false;
  341. } else {
  342. updatedColumns[key] = checked;
  343. }
  344. });
  345. setVisibleColumns(updatedColumns);
  346. };
  347. // Define all columns
  348. const allColumns = [
  349. {
  350. key: COLUMN_KEYS.TIME,
  351. title: t('时间'),
  352. dataIndex: 'timestamp2string',
  353. },
  354. {
  355. key: COLUMN_KEYS.CHANNEL,
  356. title: t('渠道'),
  357. dataIndex: 'channel',
  358. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  359. render: (text, record, index) => {
  360. return isAdminUser ? (
  361. record.type === 0 || record.type === 2 || record.type === 5 ? (
  362. <div>
  363. {
  364. <Tooltip content={record.channel_name || '[未知]'}>
  365. <Tag
  366. color={colors[parseInt(text) % colors.length]}
  367. size='large'
  368. >
  369. {' '}
  370. {text}{' '}
  371. </Tag>
  372. </Tooltip>
  373. }
  374. </div>
  375. ) : (
  376. <></>
  377. )
  378. ) : (
  379. <></>
  380. );
  381. },
  382. },
  383. {
  384. key: COLUMN_KEYS.USERNAME,
  385. title: t('用户'),
  386. dataIndex: 'username',
  387. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  388. render: (text, record, index) => {
  389. return isAdminUser ? (
  390. <div>
  391. <Avatar
  392. size='small'
  393. color={stringToColor(text)}
  394. style={{ marginRight: 4 }}
  395. onClick={(event) => {
  396. event.stopPropagation();
  397. showUserInfo(record.user_id);
  398. }}
  399. >
  400. {typeof text === 'string' && text.slice(0, 1)}
  401. </Avatar>
  402. {text}
  403. </div>
  404. ) : (
  405. <></>
  406. );
  407. },
  408. },
  409. {
  410. key: COLUMN_KEYS.TOKEN,
  411. title: t('令牌'),
  412. dataIndex: 'token_name',
  413. render: (text, record, index) => {
  414. return record.type === 0 || record.type === 2 || record.type === 5 ? (
  415. <div>
  416. <Tag
  417. color='grey'
  418. size='large'
  419. onClick={(event) => {
  420. //cancel the row click event
  421. copyText(event, text);
  422. }}
  423. >
  424. {' '}
  425. {t(text)}{' '}
  426. </Tag>
  427. </div>
  428. ) : (
  429. <></>
  430. );
  431. },
  432. },
  433. {
  434. key: COLUMN_KEYS.GROUP,
  435. title: t('分组'),
  436. dataIndex: 'group',
  437. render: (text, record, index) => {
  438. if (record.type === 0 || record.type === 2 || record.type === 5) {
  439. if (record.group) {
  440. return <>{renderGroup(record.group)}</>;
  441. } else {
  442. let other = null;
  443. try {
  444. other = JSON.parse(record.other);
  445. } catch (e) {
  446. console.error(
  447. `Failed to parse record.other: "${record.other}".`,
  448. e,
  449. );
  450. }
  451. if (other === null) {
  452. return <></>;
  453. }
  454. if (other.group !== undefined) {
  455. return <>{renderGroup(other.group)}</>;
  456. } else {
  457. return <></>;
  458. }
  459. }
  460. } else {
  461. return <></>;
  462. }
  463. },
  464. },
  465. {
  466. key: COLUMN_KEYS.TYPE,
  467. title: t('类型'),
  468. dataIndex: 'type',
  469. render: (text, record, index) => {
  470. return <>{renderType(text)}</>;
  471. },
  472. },
  473. {
  474. key: COLUMN_KEYS.MODEL,
  475. title: t('模型'),
  476. dataIndex: 'model_name',
  477. render: (text, record, index) => {
  478. return record.type === 0 || record.type === 2 || record.type === 5 ? (
  479. <>{renderModelName(record)}</>
  480. ) : (
  481. <></>
  482. );
  483. },
  484. },
  485. {
  486. key: COLUMN_KEYS.USE_TIME,
  487. title: t('用时/首字'),
  488. dataIndex: 'use_time',
  489. render: (text, record, index) => {
  490. if (record.is_stream) {
  491. let other = getLogOther(record.other);
  492. return (
  493. <>
  494. <Space>
  495. {renderUseTime(text)}
  496. {renderFirstUseTime(other?.frt)}
  497. {renderIsStream(record.is_stream)}
  498. </Space>
  499. </>
  500. );
  501. } else {
  502. return (
  503. <>
  504. <Space>
  505. {renderUseTime(text)}
  506. {renderIsStream(record.is_stream)}
  507. </Space>
  508. </>
  509. );
  510. }
  511. },
  512. },
  513. {
  514. key: COLUMN_KEYS.PROMPT,
  515. title: t('提示'),
  516. dataIndex: 'prompt_tokens',
  517. render: (text, record, index) => {
  518. return record.type === 0 || record.type === 2 || record.type === 5 ? (
  519. <>{<span> {text} </span>}</>
  520. ) : (
  521. <></>
  522. );
  523. },
  524. },
  525. {
  526. key: COLUMN_KEYS.COMPLETION,
  527. title: t('补全'),
  528. dataIndex: 'completion_tokens',
  529. render: (text, record, index) => {
  530. return parseInt(text) > 0 &&
  531. (record.type === 0 || record.type === 2 || record.type === 5) ? (
  532. <>{<span> {text} </span>}</>
  533. ) : (
  534. <></>
  535. );
  536. },
  537. },
  538. {
  539. key: COLUMN_KEYS.COST,
  540. title: t('花费'),
  541. dataIndex: 'quota',
  542. render: (text, record, index) => {
  543. return record.type === 0 || record.type === 2 || record.type === 5 ? (
  544. <>{renderQuota(text, 6)}</>
  545. ) : (
  546. <></>
  547. );
  548. },
  549. },
  550. {
  551. key: COLUMN_KEYS.RETRY,
  552. title: t('重试'),
  553. dataIndex: 'retry',
  554. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  555. render: (text, record, index) => {
  556. let content = t('渠道') + `:${record.channel}`;
  557. if (record.other !== '') {
  558. let other = JSON.parse(record.other);
  559. if (other === null) {
  560. return <></>;
  561. }
  562. if (other.admin_info !== undefined) {
  563. if (
  564. other.admin_info.use_channel !== null &&
  565. other.admin_info.use_channel !== undefined &&
  566. other.admin_info.use_channel !== ''
  567. ) {
  568. // channel id array
  569. let useChannel = other.admin_info.use_channel;
  570. let useChannelStr = useChannel.join('->');
  571. content = t('渠道') + `:${useChannelStr}`;
  572. }
  573. }
  574. }
  575. return isAdminUser ? <div>{content}</div> : <></>;
  576. },
  577. },
  578. {
  579. key: COLUMN_KEYS.DETAILS,
  580. title: t('详情'),
  581. dataIndex: 'content',
  582. render: (text, record, index) => {
  583. let other = getLogOther(record.other);
  584. if (other == null || record.type !== 2) {
  585. return (
  586. <Paragraph
  587. ellipsis={{
  588. rows: 2,
  589. showTooltip: {
  590. type: 'popover',
  591. opts: { style: { width: 240 } },
  592. },
  593. }}
  594. style={{ maxWidth: 240 }}
  595. >
  596. {text}
  597. </Paragraph>
  598. );
  599. }
  600. let content = other?.claude
  601. ? renderClaudeModelPriceSimple(
  602. other.model_ratio,
  603. other.model_price,
  604. other.group_ratio,
  605. other.cache_tokens || 0,
  606. other.cache_ratio || 1.0,
  607. other.cache_creation_tokens || 0,
  608. other.cache_creation_ratio || 1.0,
  609. )
  610. : renderModelPriceSimple(
  611. other.model_ratio,
  612. other.model_price,
  613. other.group_ratio,
  614. other.cache_tokens || 0,
  615. other.cache_ratio || 1.0,
  616. );
  617. return (
  618. <Paragraph
  619. ellipsis={{
  620. rows: 2,
  621. }}
  622. style={{ maxWidth: 240 }}
  623. >
  624. {content}
  625. </Paragraph>
  626. );
  627. },
  628. },
  629. ];
  630. // Update table when column visibility changes
  631. useEffect(() => {
  632. if (Object.keys(visibleColumns).length > 0) {
  633. // Save to localStorage
  634. localStorage.setItem(
  635. 'logs-table-columns',
  636. JSON.stringify(visibleColumns),
  637. );
  638. }
  639. }, [visibleColumns]);
  640. // Filter columns based on visibility settings
  641. const getVisibleColumns = () => {
  642. return allColumns.filter((column) => visibleColumns[column.key]);
  643. };
  644. // Column selector modal
  645. const renderColumnSelector = () => {
  646. return (
  647. <Modal
  648. title={t('列设置')}
  649. visible={showColumnSelector}
  650. onCancel={() => setShowColumnSelector(false)}
  651. footer={
  652. <>
  653. <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
  654. <Button onClick={() => setShowColumnSelector(false)}>
  655. {t('取消')}
  656. </Button>
  657. <Button type='primary' onClick={() => setShowColumnSelector(false)}>
  658. {t('确定')}
  659. </Button>
  660. </>
  661. }
  662. >
  663. <div style={{ marginBottom: 20 }}>
  664. <Checkbox
  665. checked={Object.values(visibleColumns).every((v) => v === true)}
  666. indeterminate={
  667. Object.values(visibleColumns).some((v) => v === true) &&
  668. !Object.values(visibleColumns).every((v) => v === true)
  669. }
  670. onChange={(e) => handleSelectAll(e.target.checked)}
  671. >
  672. {t('全选')}
  673. </Checkbox>
  674. </div>
  675. <div
  676. style={{
  677. display: 'flex',
  678. flexWrap: 'wrap',
  679. maxHeight: '400px',
  680. overflowY: 'auto',
  681. border: '1px solid var(--semi-color-border)',
  682. borderRadius: '6px',
  683. padding: '16px',
  684. }}
  685. >
  686. {allColumns.map((column) => {
  687. // Skip admin-only columns for non-admin users
  688. if (
  689. !isAdminUser &&
  690. (column.key === COLUMN_KEYS.CHANNEL ||
  691. column.key === COLUMN_KEYS.USERNAME ||
  692. column.key === COLUMN_KEYS.RETRY)
  693. ) {
  694. return null;
  695. }
  696. return (
  697. <div
  698. key={column.key}
  699. style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}
  700. >
  701. <Checkbox
  702. checked={!!visibleColumns[column.key]}
  703. onChange={(e) =>
  704. handleColumnVisibilityChange(column.key, e.target.checked)
  705. }
  706. >
  707. {column.title}
  708. </Checkbox>
  709. </div>
  710. );
  711. })}
  712. </div>
  713. </Modal>
  714. );
  715. };
  716. const [styleState, styleDispatch] = useContext(StyleContext);
  717. const [logs, setLogs] = useState([]);
  718. const [expandData, setExpandData] = useState({});
  719. const [showStat, setShowStat] = useState(false);
  720. const [loading, setLoading] = useState(false);
  721. const [loadingStat, setLoadingStat] = useState(false);
  722. const [activePage, setActivePage] = useState(1);
  723. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  724. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  725. const [logType, setLogType] = useState(0);
  726. const isAdminUser = isAdmin();
  727. let now = new Date();
  728. // 初始化start_timestamp为今天0点
  729. const [inputs, setInputs] = useState({
  730. username: '',
  731. token_name: '',
  732. model_name: '',
  733. start_timestamp: timestamp2string(getTodayStartTimestamp()),
  734. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  735. channel: '',
  736. group: '',
  737. });
  738. const {
  739. username,
  740. token_name,
  741. model_name,
  742. start_timestamp,
  743. end_timestamp,
  744. channel,
  745. group,
  746. } = inputs;
  747. const [stat, setStat] = useState({
  748. quota: 0,
  749. token: 0,
  750. });
  751. const handleInputChange = (value, name) => {
  752. setInputs((inputs) => ({ ...inputs, [name]: value }));
  753. };
  754. const getLogSelfStat = async () => {
  755. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  756. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  757. let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  758. url = encodeURI(url);
  759. let res = await API.get(url);
  760. const { success, message, data } = res.data;
  761. if (success) {
  762. setStat(data);
  763. } else {
  764. showError(message);
  765. }
  766. };
  767. const getLogStat = async () => {
  768. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  769. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  770. 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}&group=${group}`;
  771. url = encodeURI(url);
  772. let res = await API.get(url);
  773. const { success, message, data } = res.data;
  774. if (success) {
  775. setStat(data);
  776. } else {
  777. showError(message);
  778. }
  779. };
  780. const handleEyeClick = async () => {
  781. if (loadingStat) {
  782. return;
  783. }
  784. setLoadingStat(true);
  785. if (isAdminUser) {
  786. await getLogStat();
  787. } else {
  788. await getLogSelfStat();
  789. }
  790. setShowStat(true);
  791. setLoadingStat(false);
  792. };
  793. const showUserInfo = async (userId) => {
  794. if (!isAdminUser) {
  795. return;
  796. }
  797. const res = await API.get(`/api/user/${userId}`);
  798. const { success, message, data } = res.data;
  799. if (success) {
  800. Modal.info({
  801. title: t('用户信息'),
  802. content: (
  803. <div style={{ padding: 12 }}>
  804. <p>
  805. {t('用户名')}: {data.username}
  806. </p>
  807. <p>
  808. {t('余额')}: {renderQuota(data.quota)}
  809. </p>
  810. <p>
  811. {t('已用额度')}:{renderQuota(data.used_quota)}
  812. </p>
  813. <p>
  814. {t('请求次数')}:{renderNumber(data.request_count)}
  815. </p>
  816. </div>
  817. ),
  818. centered: true,
  819. });
  820. } else {
  821. showError(message);
  822. }
  823. };
  824. const setLogsFormat = (logs) => {
  825. let expandDatesLocal = {};
  826. for (let i = 0; i < logs.length; i++) {
  827. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  828. logs[i].key = logs[i].id;
  829. let other = getLogOther(logs[i].other);
  830. let expandDataLocal = [];
  831. if (isAdmin()) {
  832. // let content = '渠道:' + logs[i].channel;
  833. // if (other.admin_info !== undefined) {
  834. // if (
  835. // other.admin_info.use_channel !== null &&
  836. // other.admin_info.use_channel !== undefined &&
  837. // other.admin_info.use_channel !== ''
  838. // ) {
  839. // // channel id array
  840. // let useChannel = other.admin_info.use_channel;
  841. // let useChannelStr = useChannel.join('->');
  842. // content = `渠道:${useChannelStr}`;
  843. // }
  844. // }
  845. // expandDataLocal.push({
  846. // key: '渠道重试',
  847. // value: content,
  848. // })
  849. }
  850. if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
  851. expandDataLocal.push({
  852. key: t('渠道信息'),
  853. value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
  854. });
  855. }
  856. if (other?.ws || other?.audio) {
  857. expandDataLocal.push({
  858. key: t('语音输入'),
  859. value: other.audio_input,
  860. });
  861. expandDataLocal.push({
  862. key: t('语音输出'),
  863. value: other.audio_output,
  864. });
  865. expandDataLocal.push({
  866. key: t('文字输入'),
  867. value: other.text_input,
  868. });
  869. expandDataLocal.push({
  870. key: t('文字输出'),
  871. value: other.text_output,
  872. });
  873. }
  874. if (other?.cache_tokens > 0) {
  875. expandDataLocal.push({
  876. key: t('缓存 Tokens'),
  877. value: other.cache_tokens,
  878. });
  879. }
  880. if (other?.cache_creation_tokens > 0) {
  881. expandDataLocal.push({
  882. key: t('缓存创建 Tokens'),
  883. value: other.cache_creation_tokens,
  884. });
  885. }
  886. if (logs[i].type === 2) {
  887. expandDataLocal.push({
  888. key: t('日志详情'),
  889. value: other?.claude
  890. ? renderClaudeLogContent(
  891. other?.model_ratio,
  892. other.completion_ratio,
  893. other.model_price,
  894. other.group_ratio,
  895. other.cache_ratio || 1.0,
  896. other.cache_creation_ratio || 1.0,
  897. )
  898. : renderLogContent(
  899. other?.model_ratio,
  900. other.completion_ratio,
  901. other.model_price,
  902. other.group_ratio,
  903. other?.user_group_ratio,
  904. false,
  905. 1.0,
  906. undefined,
  907. other.web_search || false,
  908. other.web_search_call_count || 0,
  909. other.file_search || false,
  910. other.file_search_call_count || 0,
  911. ),
  912. });
  913. }
  914. if (logs[i].type === 2) {
  915. let modelMapped =
  916. other?.is_model_mapped &&
  917. other?.upstream_model_name &&
  918. other?.upstream_model_name !== '';
  919. if (modelMapped) {
  920. expandDataLocal.push({
  921. key: t('请求并计费模型'),
  922. value: logs[i].model_name,
  923. });
  924. expandDataLocal.push({
  925. key: t('实际模型'),
  926. value: other.upstream_model_name,
  927. });
  928. }
  929. let content = '';
  930. if (other?.ws || other?.audio) {
  931. content = renderAudioModelPrice(
  932. other?.text_input,
  933. other?.text_output,
  934. other?.model_ratio,
  935. other?.model_price,
  936. other?.completion_ratio,
  937. other?.audio_input,
  938. other?.audio_output,
  939. other?.audio_ratio,
  940. other?.audio_completion_ratio,
  941. other?.group_ratio,
  942. other?.cache_tokens || 0,
  943. other?.cache_ratio || 1.0,
  944. );
  945. } else if (other?.claude) {
  946. content = renderClaudeModelPrice(
  947. logs[i].prompt_tokens,
  948. logs[i].completion_tokens,
  949. other.model_ratio,
  950. other.model_price,
  951. other.completion_ratio,
  952. other.group_ratio,
  953. other.cache_tokens || 0,
  954. other.cache_ratio || 1.0,
  955. other.cache_creation_tokens || 0,
  956. other.cache_creation_ratio || 1.0,
  957. );
  958. } else {
  959. content = renderModelPrice(
  960. logs[i].prompt_tokens,
  961. logs[i].completion_tokens,
  962. other?.model_ratio,
  963. other?.model_price,
  964. other?.completion_ratio,
  965. other?.group_ratio,
  966. other?.cache_tokens || 0,
  967. other?.cache_ratio || 1.0,
  968. other?.image || false,
  969. other?.image_ratio || 0,
  970. other?.image_output || 0,
  971. other?.web_search || false,
  972. other?.web_search_call_count || 0,
  973. other?.web_search_price || 0,
  974. other?.file_search || false,
  975. other?.file_search_call_count || 0,
  976. other?.file_search_price || 0,
  977. );
  978. }
  979. expandDataLocal.push({
  980. key: t('计费过程'),
  981. value: content,
  982. });
  983. if (other?.reasoning_effort) {
  984. expandDataLocal.push({
  985. key: t('Reasoning Effort'),
  986. value: other.reasoning_effort,
  987. });
  988. }
  989. }
  990. expandDatesLocal[logs[i].key] = expandDataLocal;
  991. }
  992. setExpandData(expandDatesLocal);
  993. setLogs(logs);
  994. };
  995. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  996. setLoading(true);
  997. let url = '';
  998. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  999. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  1000. if (isAdminUser) {
  1001. 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}&group=${group}`;
  1002. } else {
  1003. 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}&group=${group}`;
  1004. }
  1005. url = encodeURI(url);
  1006. const res = await API.get(url);
  1007. const { success, message, data } = res.data;
  1008. if (success) {
  1009. const newPageData = data.items;
  1010. setActivePage(data.page);
  1011. setPageSize(data.page_size);
  1012. setLogCount(data.total);
  1013. setLogsFormat(newPageData);
  1014. } else {
  1015. showError(message);
  1016. }
  1017. setLoading(false);
  1018. };
  1019. const handlePageChange = (page) => {
  1020. setActivePage(page);
  1021. loadLogs(page, pageSize, logType).then((r) => {});
  1022. };
  1023. const handlePageSizeChange = async (size) => {
  1024. localStorage.setItem('page-size', size + '');
  1025. setPageSize(size);
  1026. setActivePage(1);
  1027. loadLogs(activePage, size)
  1028. .then()
  1029. .catch((reason) => {
  1030. showError(reason);
  1031. });
  1032. };
  1033. const refresh = async () => {
  1034. setActivePage(1);
  1035. handleEyeClick();
  1036. await loadLogs(activePage, pageSize, logType);
  1037. };
  1038. const copyText = async (e, text) => {
  1039. e.stopPropagation();
  1040. if (await copy(text)) {
  1041. showSuccess('已复制:' + text);
  1042. } else {
  1043. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  1044. }
  1045. };
  1046. useEffect(() => {
  1047. const localPageSize =
  1048. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  1049. setPageSize(localPageSize);
  1050. loadLogs(activePage, localPageSize)
  1051. .then()
  1052. .catch((reason) => {
  1053. showError(reason);
  1054. });
  1055. handleEyeClick();
  1056. }, []);
  1057. const expandRowRender = (record, index) => {
  1058. return <Descriptions data={expandData[record.key]} />;
  1059. };
  1060. return (
  1061. <>
  1062. {renderColumnSelector()}
  1063. <Layout>
  1064. <Header>
  1065. <Spin spinning={loadingStat}>
  1066. <Space>
  1067. <Tag
  1068. color='blue'
  1069. size='large'
  1070. style={{
  1071. padding: 15,
  1072. borderRadius: '8px',
  1073. fontWeight: 500,
  1074. boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
  1075. }}
  1076. >
  1077. {t('消耗额度')}: {renderQuota(stat.quota)}
  1078. </Tag>
  1079. <Tag
  1080. color='pink'
  1081. size='large'
  1082. style={{
  1083. padding: 15,
  1084. borderRadius: '8px',
  1085. fontWeight: 500,
  1086. boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
  1087. }}
  1088. >
  1089. RPM: {stat.rpm}
  1090. </Tag>
  1091. <Tag
  1092. color='white'
  1093. size='large'
  1094. style={{
  1095. padding: 15,
  1096. border: 'none',
  1097. boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
  1098. borderRadius: '8px',
  1099. fontWeight: 500,
  1100. }}
  1101. >
  1102. TPM: {stat.tpm}
  1103. </Tag>
  1104. </Space>
  1105. </Spin>
  1106. </Header>
  1107. <Form layout='horizontal' style={{ marginTop: 10 }}>
  1108. <>
  1109. <Form.Section>
  1110. <div style={{ marginBottom: 10 }}>
  1111. {styleState.isMobile ? (
  1112. <div>
  1113. <Form.DatePicker
  1114. field='start_timestamp'
  1115. label={t('起始时间')}
  1116. style={{ width: 272 }}
  1117. initValue={start_timestamp}
  1118. type='dateTime'
  1119. onChange={(value) => {
  1120. console.log(value);
  1121. handleInputChange(value, 'start_timestamp');
  1122. }}
  1123. />
  1124. <Form.DatePicker
  1125. field='end_timestamp'
  1126. fluid
  1127. label={t('结束时间')}
  1128. style={{ width: 272 }}
  1129. initValue={end_timestamp}
  1130. type='dateTime'
  1131. onChange={(value) =>
  1132. handleInputChange(value, 'end_timestamp')
  1133. }
  1134. />
  1135. </div>
  1136. ) : (
  1137. <Form.DatePicker
  1138. field='range_timestamp'
  1139. label={t('时间范围')}
  1140. initValue={[start_timestamp, end_timestamp]}
  1141. type='dateTimeRange'
  1142. name='range_timestamp'
  1143. onChange={(value) => {
  1144. if (Array.isArray(value) && value.length === 2) {
  1145. handleInputChange(value[0], 'start_timestamp');
  1146. handleInputChange(value[1], 'end_timestamp');
  1147. }
  1148. }}
  1149. />
  1150. )}
  1151. </div>
  1152. </Form.Section>
  1153. <Form.Input
  1154. field='token_name'
  1155. label={t('令牌名称')}
  1156. value={token_name}
  1157. placeholder={t('可选值')}
  1158. name='token_name'
  1159. onChange={(value) => handleInputChange(value, 'token_name')}
  1160. />
  1161. <Form.Input
  1162. field='model_name'
  1163. label={t('模型名称')}
  1164. value={model_name}
  1165. placeholder={t('可选值')}
  1166. name='model_name'
  1167. onChange={(value) => handleInputChange(value, 'model_name')}
  1168. />
  1169. <Form.Input
  1170. field='group'
  1171. label={t('分组')}
  1172. value={group}
  1173. placeholder={t('可选值')}
  1174. name='group'
  1175. onChange={(value) => handleInputChange(value, 'group')}
  1176. />
  1177. {isAdminUser && (
  1178. <>
  1179. <Form.Input
  1180. field='channel'
  1181. label={t('渠道 ID')}
  1182. value={channel}
  1183. placeholder={t('可选值')}
  1184. name='channel'
  1185. onChange={(value) => handleInputChange(value, 'channel')}
  1186. />
  1187. <Form.Input
  1188. field='username'
  1189. label={t('用户名称')}
  1190. value={username}
  1191. placeholder={t('可选值')}
  1192. name='username'
  1193. onChange={(value) => handleInputChange(value, 'username')}
  1194. />
  1195. </>
  1196. )}
  1197. <Button
  1198. label={t('查询')}
  1199. type='primary'
  1200. htmlType='submit'
  1201. className='btn-margin-right'
  1202. onClick={refresh}
  1203. loading={loading}
  1204. style={{ marginTop: 24 }}
  1205. >
  1206. {t('查询')}
  1207. </Button>
  1208. <Form.Section></Form.Section>
  1209. </>
  1210. </Form>
  1211. <div style={{ marginTop: 10 }}>
  1212. <Select
  1213. defaultValue='0'
  1214. style={{ width: 120 }}
  1215. onChange={(value) => {
  1216. setLogType(parseInt(value));
  1217. loadLogs(0, pageSize, parseInt(value));
  1218. }}
  1219. >
  1220. <Select.Option value='0'>{t('全部')}</Select.Option>
  1221. <Select.Option value='1'>{t('充值')}</Select.Option>
  1222. <Select.Option value='2'>{t('消费')}</Select.Option>
  1223. <Select.Option value='3'>{t('管理')}</Select.Option>
  1224. <Select.Option value='4'>{t('系统')}</Select.Option>
  1225. <Select.Option value='5'>{t('错误')}</Select.Option>
  1226. </Select>
  1227. <Button
  1228. theme='light'
  1229. type='tertiary'
  1230. icon={<IconSetting />}
  1231. onClick={() => setShowColumnSelector(true)}
  1232. style={{ marginLeft: 8 }}
  1233. >
  1234. {t('列设置')}
  1235. </Button>
  1236. </div>
  1237. <Table
  1238. style={{ marginTop: 5 }}
  1239. columns={getVisibleColumns()}
  1240. expandedRowRender={expandRowRender}
  1241. expandRowByClick={true}
  1242. dataSource={logs}
  1243. rowKey='key'
  1244. pagination={{
  1245. formatPageText: (page) =>
  1246. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  1247. start: page.currentStart,
  1248. end: page.currentEnd,
  1249. total: logCount,
  1250. }),
  1251. currentPage: activePage,
  1252. pageSize: pageSize,
  1253. total: logCount,
  1254. pageSizeOpts: [10, 20, 50, 100],
  1255. showSizeChanger: true,
  1256. onPageSizeChange: (size) => {
  1257. handlePageSizeChange(size);
  1258. },
  1259. onPageChange: handlePageChange,
  1260. }}
  1261. />
  1262. </Layout>
  1263. </>
  1264. );
  1265. };
  1266. export default LogsTable;