useUsageLogsData.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. import { useState, useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Modal } from '@douyinfe/semi-ui';
  4. import {
  5. API,
  6. getTodayStartTimestamp,
  7. isAdmin,
  8. showError,
  9. showSuccess,
  10. timestamp2string,
  11. renderQuota,
  12. renderNumber,
  13. getLogOther,
  14. copy,
  15. renderClaudeLogContent,
  16. renderLogContent,
  17. renderAudioModelPrice,
  18. renderClaudeModelPrice,
  19. renderModelPrice
  20. } from '../../helpers';
  21. import { ITEMS_PER_PAGE } from '../../constants';
  22. import { useTableCompactMode } from '../common/useTableCompactMode';
  23. export const useLogsData = () => {
  24. const { t } = useTranslation();
  25. // Define column keys for selection
  26. const COLUMN_KEYS = {
  27. TIME: 'time',
  28. CHANNEL: 'channel',
  29. USERNAME: 'username',
  30. TOKEN: 'token',
  31. GROUP: 'group',
  32. TYPE: 'type',
  33. MODEL: 'model',
  34. USE_TIME: 'use_time',
  35. PROMPT: 'prompt',
  36. COMPLETION: 'completion',
  37. COST: 'cost',
  38. RETRY: 'retry',
  39. IP: 'ip',
  40. DETAILS: 'details',
  41. };
  42. // Basic state
  43. const [logs, setLogs] = useState([]);
  44. const [expandData, setExpandData] = useState({});
  45. const [showStat, setShowStat] = useState(false);
  46. const [loading, setLoading] = useState(false);
  47. const [loadingStat, setLoadingStat] = useState(false);
  48. const [activePage, setActivePage] = useState(1);
  49. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  50. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  51. const [logType, setLogType] = useState(0);
  52. // User and admin
  53. const isAdminUser = isAdmin();
  54. // Statistics state
  55. const [stat, setStat] = useState({
  56. quota: 0,
  57. token: 0,
  58. });
  59. // Form state
  60. const [formApi, setFormApi] = useState(null);
  61. let now = new Date();
  62. const formInitValues = {
  63. username: '',
  64. token_name: '',
  65. model_name: '',
  66. channel: '',
  67. group: '',
  68. dateRange: [
  69. timestamp2string(getTodayStartTimestamp()),
  70. timestamp2string(now.getTime() / 1000 + 3600),
  71. ],
  72. logType: '0',
  73. };
  74. // Column visibility state
  75. const [visibleColumns, setVisibleColumns] = useState({});
  76. const [showColumnSelector, setShowColumnSelector] = useState(false);
  77. // Compact mode
  78. const [compactMode, setCompactMode] = useTableCompactMode('logs');
  79. // User info modal state
  80. const [showUserInfo, setShowUserInfoModal] = useState(false);
  81. const [userInfoData, setUserInfoData] = useState(null);
  82. // Load saved column preferences from localStorage
  83. useEffect(() => {
  84. const savedColumns = localStorage.getItem('logs-table-columns');
  85. if (savedColumns) {
  86. try {
  87. const parsed = JSON.parse(savedColumns);
  88. const defaults = getDefaultColumnVisibility();
  89. const merged = { ...defaults, ...parsed };
  90. setVisibleColumns(merged);
  91. } catch (e) {
  92. console.error('Failed to parse saved column preferences', e);
  93. initDefaultColumns();
  94. }
  95. } else {
  96. initDefaultColumns();
  97. }
  98. }, []);
  99. // Get default column visibility based on user role
  100. const getDefaultColumnVisibility = () => {
  101. return {
  102. [COLUMN_KEYS.TIME]: true,
  103. [COLUMN_KEYS.CHANNEL]: isAdminUser,
  104. [COLUMN_KEYS.USERNAME]: isAdminUser,
  105. [COLUMN_KEYS.TOKEN]: true,
  106. [COLUMN_KEYS.GROUP]: true,
  107. [COLUMN_KEYS.TYPE]: true,
  108. [COLUMN_KEYS.MODEL]: true,
  109. [COLUMN_KEYS.USE_TIME]: true,
  110. [COLUMN_KEYS.PROMPT]: true,
  111. [COLUMN_KEYS.COMPLETION]: true,
  112. [COLUMN_KEYS.COST]: true,
  113. [COLUMN_KEYS.RETRY]: isAdminUser,
  114. [COLUMN_KEYS.IP]: true,
  115. [COLUMN_KEYS.DETAILS]: true,
  116. };
  117. };
  118. // Initialize default column visibility
  119. const initDefaultColumns = () => {
  120. const defaults = getDefaultColumnVisibility();
  121. setVisibleColumns(defaults);
  122. localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
  123. };
  124. // Handle column visibility change
  125. const handleColumnVisibilityChange = (columnKey, checked) => {
  126. const updatedColumns = { ...visibleColumns, [columnKey]: checked };
  127. setVisibleColumns(updatedColumns);
  128. };
  129. // Handle "Select All" checkbox
  130. const handleSelectAll = (checked) => {
  131. const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
  132. const updatedColumns = {};
  133. allKeys.forEach((key) => {
  134. if (
  135. (key === COLUMN_KEYS.CHANNEL ||
  136. key === COLUMN_KEYS.USERNAME ||
  137. key === COLUMN_KEYS.RETRY) &&
  138. !isAdminUser
  139. ) {
  140. updatedColumns[key] = false;
  141. } else {
  142. updatedColumns[key] = checked;
  143. }
  144. });
  145. setVisibleColumns(updatedColumns);
  146. };
  147. // Update table when column visibility changes
  148. useEffect(() => {
  149. if (Object.keys(visibleColumns).length > 0) {
  150. localStorage.setItem(
  151. 'logs-table-columns',
  152. JSON.stringify(visibleColumns),
  153. );
  154. }
  155. }, [visibleColumns]);
  156. // 获取表单值的辅助函数,确保所有值都是字符串
  157. const getFormValues = () => {
  158. const formValues = formApi ? formApi.getValues() : {};
  159. let start_timestamp = timestamp2string(getTodayStartTimestamp());
  160. let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
  161. if (
  162. formValues.dateRange &&
  163. Array.isArray(formValues.dateRange) &&
  164. formValues.dateRange.length === 2
  165. ) {
  166. start_timestamp = formValues.dateRange[0];
  167. end_timestamp = formValues.dateRange[1];
  168. }
  169. return {
  170. username: formValues.username || '',
  171. token_name: formValues.token_name || '',
  172. model_name: formValues.model_name || '',
  173. start_timestamp,
  174. end_timestamp,
  175. channel: formValues.channel || '',
  176. group: formValues.group || '',
  177. logType: formValues.logType ? parseInt(formValues.logType) : 0,
  178. };
  179. };
  180. // Statistics functions
  181. const getLogSelfStat = async () => {
  182. const {
  183. token_name,
  184. model_name,
  185. start_timestamp,
  186. end_timestamp,
  187. group,
  188. logType: formLogType,
  189. } = getFormValues();
  190. const currentLogType = formLogType !== undefined ? formLogType : logType;
  191. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  192. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  193. let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  194. url = encodeURI(url);
  195. let res = await API.get(url);
  196. const { success, message, data } = res.data;
  197. if (success) {
  198. setStat(data);
  199. } else {
  200. showError(message);
  201. }
  202. };
  203. const getLogStat = async () => {
  204. const {
  205. username,
  206. token_name,
  207. model_name,
  208. start_timestamp,
  209. end_timestamp,
  210. channel,
  211. group,
  212. logType: formLogType,
  213. } = getFormValues();
  214. const currentLogType = formLogType !== undefined ? formLogType : logType;
  215. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  216. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  217. let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
  218. url = encodeURI(url);
  219. let res = await API.get(url);
  220. const { success, message, data } = res.data;
  221. if (success) {
  222. setStat(data);
  223. } else {
  224. showError(message);
  225. }
  226. };
  227. const handleEyeClick = async () => {
  228. if (loadingStat) {
  229. return;
  230. }
  231. setLoadingStat(true);
  232. if (isAdminUser) {
  233. await getLogStat();
  234. } else {
  235. await getLogSelfStat();
  236. }
  237. setShowStat(true);
  238. setLoadingStat(false);
  239. };
  240. // User info function
  241. const showUserInfoFunc = async (userId) => {
  242. if (!isAdminUser) {
  243. return;
  244. }
  245. const res = await API.get(`/api/user/${userId}`);
  246. const { success, message, data } = res.data;
  247. if (success) {
  248. setUserInfoData(data);
  249. setShowUserInfoModal(true);
  250. } else {
  251. showError(message);
  252. }
  253. };
  254. // Format logs data
  255. const setLogsFormat = (logs) => {
  256. let expandDatesLocal = {};
  257. for (let i = 0; i < logs.length; i++) {
  258. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  259. logs[i].key = logs[i].id;
  260. let other = getLogOther(logs[i].other);
  261. let expandDataLocal = [];
  262. if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
  263. expandDataLocal.push({
  264. key: t('渠道信息'),
  265. value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
  266. });
  267. }
  268. if (other?.ws || other?.audio) {
  269. expandDataLocal.push({
  270. key: t('语音输入'),
  271. value: other.audio_input,
  272. });
  273. expandDataLocal.push({
  274. key: t('语音输出'),
  275. value: other.audio_output,
  276. });
  277. expandDataLocal.push({
  278. key: t('文字输入'),
  279. value: other.text_input,
  280. });
  281. expandDataLocal.push({
  282. key: t('文字输出'),
  283. value: other.text_output,
  284. });
  285. }
  286. if (other?.cache_tokens > 0) {
  287. expandDataLocal.push({
  288. key: t('缓存 Tokens'),
  289. value: other.cache_tokens,
  290. });
  291. }
  292. if (other?.cache_creation_tokens > 0) {
  293. expandDataLocal.push({
  294. key: t('缓存创建 Tokens'),
  295. value: other.cache_creation_tokens,
  296. });
  297. }
  298. if (logs[i].type === 2) {
  299. expandDataLocal.push({
  300. key: t('日志详情'),
  301. value: other?.claude
  302. ? renderClaudeLogContent(
  303. other?.model_ratio,
  304. other.completion_ratio,
  305. other.model_price,
  306. other.group_ratio,
  307. other?.user_group_ratio,
  308. other.cache_ratio || 1.0,
  309. other.cache_creation_ratio || 1.0,
  310. )
  311. : renderLogContent(
  312. other?.model_ratio,
  313. other.completion_ratio,
  314. other.model_price,
  315. other.group_ratio,
  316. other?.user_group_ratio,
  317. false,
  318. 1.0,
  319. other.web_search || false,
  320. other.web_search_call_count || 0,
  321. other.file_search || false,
  322. other.file_search_call_count || 0,
  323. ),
  324. });
  325. }
  326. if (logs[i].type === 2) {
  327. let modelMapped =
  328. other?.is_model_mapped &&
  329. other?.upstream_model_name &&
  330. other?.upstream_model_name !== '';
  331. if (modelMapped) {
  332. expandDataLocal.push({
  333. key: t('请求并计费模型'),
  334. value: logs[i].model_name,
  335. });
  336. expandDataLocal.push({
  337. key: t('实际模型'),
  338. value: other.upstream_model_name,
  339. });
  340. }
  341. let content = '';
  342. if (other?.ws || other?.audio) {
  343. content = renderAudioModelPrice(
  344. other?.text_input,
  345. other?.text_output,
  346. other?.model_ratio,
  347. other?.model_price,
  348. other?.completion_ratio,
  349. other?.audio_input,
  350. other?.audio_output,
  351. other?.audio_ratio,
  352. other?.audio_completion_ratio,
  353. other?.group_ratio,
  354. other?.user_group_ratio,
  355. other?.cache_tokens || 0,
  356. other?.cache_ratio || 1.0,
  357. );
  358. } else if (other?.claude) {
  359. content = renderClaudeModelPrice(
  360. logs[i].prompt_tokens,
  361. logs[i].completion_tokens,
  362. other.model_ratio,
  363. other.model_price,
  364. other.completion_ratio,
  365. other.group_ratio,
  366. other?.user_group_ratio,
  367. other.cache_tokens || 0,
  368. other.cache_ratio || 1.0,
  369. other.cache_creation_tokens || 0,
  370. other.cache_creation_ratio || 1.0,
  371. );
  372. } else {
  373. content = renderModelPrice(
  374. logs[i].prompt_tokens,
  375. logs[i].completion_tokens,
  376. other?.model_ratio,
  377. other?.model_price,
  378. other?.completion_ratio,
  379. other?.group_ratio,
  380. other?.user_group_ratio,
  381. other?.cache_tokens || 0,
  382. other?.cache_ratio || 1.0,
  383. other?.image || false,
  384. other?.image_ratio || 0,
  385. other?.image_output || 0,
  386. other?.web_search || false,
  387. other?.web_search_call_count || 0,
  388. other?.web_search_price || 0,
  389. other?.file_search || false,
  390. other?.file_search_call_count || 0,
  391. other?.file_search_price || 0,
  392. other?.audio_input_seperate_price || false,
  393. other?.audio_input_token_count || 0,
  394. other?.audio_input_price || 0,
  395. );
  396. }
  397. expandDataLocal.push({
  398. key: t('计费过程'),
  399. value: content,
  400. });
  401. if (other?.reasoning_effort) {
  402. expandDataLocal.push({
  403. key: t('Reasoning Effort'),
  404. value: other.reasoning_effort,
  405. });
  406. }
  407. }
  408. expandDatesLocal[logs[i].key] = expandDataLocal;
  409. }
  410. setExpandData(expandDatesLocal);
  411. setLogs(logs);
  412. };
  413. // Load logs function
  414. const loadLogs = async (startIdx, pageSize, customLogType = null) => {
  415. setLoading(true);
  416. let url = '';
  417. const {
  418. username,
  419. token_name,
  420. model_name,
  421. start_timestamp,
  422. end_timestamp,
  423. channel,
  424. group,
  425. logType: formLogType,
  426. } = getFormValues();
  427. const currentLogType =
  428. customLogType !== null
  429. ? customLogType
  430. : formLogType !== undefined
  431. ? formLogType
  432. : logType;
  433. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  434. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  435. if (isAdminUser) {
  436. url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
  437. } else {
  438. url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  439. }
  440. url = encodeURI(url);
  441. const res = await API.get(url);
  442. const { success, message, data } = res.data;
  443. if (success) {
  444. const newPageData = data.items;
  445. setActivePage(data.page);
  446. setPageSize(data.page_size);
  447. setLogCount(data.total);
  448. setLogsFormat(newPageData);
  449. } else {
  450. showError(message);
  451. }
  452. setLoading(false);
  453. };
  454. // Page handlers
  455. const handlePageChange = (page) => {
  456. setActivePage(page);
  457. loadLogs(page, pageSize).then((r) => { });
  458. };
  459. const handlePageSizeChange = async (size) => {
  460. localStorage.setItem('page-size', size + '');
  461. setPageSize(size);
  462. setActivePage(1);
  463. loadLogs(activePage, size)
  464. .then()
  465. .catch((reason) => {
  466. showError(reason);
  467. });
  468. };
  469. // Refresh function
  470. const refresh = async () => {
  471. setActivePage(1);
  472. handleEyeClick();
  473. await loadLogs(1, pageSize);
  474. };
  475. // Copy text function
  476. const copyText = async (e, text) => {
  477. e.stopPropagation();
  478. if (await copy(text)) {
  479. showSuccess('已复制:' + text);
  480. } else {
  481. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  482. }
  483. };
  484. // Initialize data
  485. useEffect(() => {
  486. const localPageSize =
  487. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  488. setPageSize(localPageSize);
  489. loadLogs(activePage, localPageSize)
  490. .then()
  491. .catch((reason) => {
  492. showError(reason);
  493. });
  494. }, []);
  495. // Initialize statistics when formApi is available
  496. useEffect(() => {
  497. if (formApi) {
  498. handleEyeClick();
  499. }
  500. }, [formApi]);
  501. // Check if any record has expandable content
  502. const hasExpandableRows = () => {
  503. return logs.some(
  504. (log) => expandData[log.key] && expandData[log.key].length > 0,
  505. );
  506. };
  507. return {
  508. // Basic state
  509. logs,
  510. expandData,
  511. showStat,
  512. loading,
  513. loadingStat,
  514. activePage,
  515. logCount,
  516. pageSize,
  517. logType,
  518. stat,
  519. isAdminUser,
  520. // Form state
  521. formApi,
  522. setFormApi,
  523. formInitValues,
  524. getFormValues,
  525. // Column visibility
  526. visibleColumns,
  527. showColumnSelector,
  528. setShowColumnSelector,
  529. handleColumnVisibilityChange,
  530. handleSelectAll,
  531. initDefaultColumns,
  532. COLUMN_KEYS,
  533. // Compact mode
  534. compactMode,
  535. setCompactMode,
  536. // User info modal
  537. showUserInfo,
  538. setShowUserInfoModal,
  539. userInfoData,
  540. showUserInfoFunc,
  541. // Functions
  542. loadLogs,
  543. handlePageChange,
  544. handlePageSizeChange,
  545. refresh,
  546. copyText,
  547. handleEyeClick,
  548. setLogsFormat,
  549. hasExpandableRows,
  550. setLogType,
  551. // Translation
  552. t,
  553. };
  554. };