useUsageLogsData.js 17 KB

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