LogsTable.js 35 KB

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