LogsTable.js 35 KB

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