diff --git a/controller/channel-test.go b/controller/channel-test.go index 02a30593d..ba8a8490a 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -83,6 +83,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr c.Request.Header.Set("Content-Type", "application/json") c.Set("channel", channel.Type) c.Set("base_url", channel.GetBaseURL()) + group, _ := model.GetUserGroup(1, false) + c.Set("group", group) middleware.SetupContextForSelectedChannel(c, channel, testModel) @@ -158,7 +160,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr tok := time.Now() milliseconds := tok.Sub(tik).Milliseconds() consumedTime := float64(milliseconds) / 1000.0 - other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, 0, 0.0, priceData.ModelPrice) + other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, + usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice) model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试", quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other) common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody))) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 94cd6ba8a..2cd6c15d0 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -29,10 +29,12 @@ import { Table, Tag, Tooltip, - Typography + Typography, + Checkbox, + Layout } from '@douyinfe/semi-ui'; import EditChannel from '../pages/Channel/EditChannel'; -import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons'; +import { IconList, IconTreeTriangleDown, IconClose, IconFilter, IconPlus, IconRefresh, IconSetting } from '@douyinfe/semi-icons'; import { loadChannelModels } from './utils.js'; import EditTagModal from '../pages/Channel/EditTagModal.js'; import TextNumberInput from './custom/TextNumberInput.js'; @@ -141,21 +143,105 @@ const ChannelsTable = () => { } }; - const columns = [ - // { - // title: '', - // dataIndex: 'checkbox', - // className: 'checkbox', - // }, + // Define column keys for selection + const COLUMN_KEYS = { + ID: 'id', + NAME: 'name', + GROUP: 'group', + TYPE: 'type', + STATUS: 'status', + RESPONSE_TIME: 'response_time', + BALANCE: 'balance', + PRIORITY: 'priority', + WEIGHT: 'weight', + OPERATE: 'operate' + }; + + // State for column visibility + const [visibleColumns, setVisibleColumns] = useState({}); + const [showColumnSelector, setShowColumnSelector] = useState(false); + + // Load saved column preferences from localStorage + useEffect(() => { + const savedColumns = localStorage.getItem('channels-table-columns'); + if (savedColumns) { + try { + const parsed = JSON.parse(savedColumns); + // Make sure all columns are accounted for + const defaults = getDefaultColumnVisibility(); + const merged = { ...defaults, ...parsed }; + setVisibleColumns(merged); + } catch (e) { + console.error('Failed to parse saved column preferences', e); + initDefaultColumns(); + } + } else { + initDefaultColumns(); + } + }, []); + + // Update table when column visibility changes + useEffect(() => { + if (Object.keys(visibleColumns).length > 0) { + // Save to localStorage + localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns)); + } + }, [visibleColumns]); + + // Get default column visibility + const getDefaultColumnVisibility = () => { + return { + [COLUMN_KEYS.ID]: true, + [COLUMN_KEYS.NAME]: true, + [COLUMN_KEYS.GROUP]: true, + [COLUMN_KEYS.TYPE]: true, + [COLUMN_KEYS.STATUS]: true, + [COLUMN_KEYS.RESPONSE_TIME]: true, + [COLUMN_KEYS.BALANCE]: true, + [COLUMN_KEYS.PRIORITY]: true, + [COLUMN_KEYS.WEIGHT]: true, + [COLUMN_KEYS.OPERATE]: true + }; + }; + + // Initialize default column visibility + const initDefaultColumns = () => { + const defaults = getDefaultColumnVisibility(); + setVisibleColumns(defaults); + }; + + // Handle column visibility change + const handleColumnVisibilityChange = (columnKey, checked) => { + const updatedColumns = { ...visibleColumns, [columnKey]: checked }; + setVisibleColumns(updatedColumns); + }; + + // Handle "Select All" checkbox + const handleSelectAll = (checked) => { + const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]); + const updatedColumns = {}; + + allKeys.forEach(key => { + updatedColumns[key] = checked; + }); + + setVisibleColumns(updatedColumns); + }; + + // Define all columns with keys + const allColumns = [ { + key: COLUMN_KEYS.ID, title: t('ID'), dataIndex: 'id' }, { + key: COLUMN_KEYS.NAME, title: t('名称'), dataIndex: 'name' }, { + key: COLUMN_KEYS.GROUP, title: t('分组'), dataIndex: 'group', render: (text, record, index) => { @@ -177,6 +263,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.TYPE, title: t('类型'), dataIndex: 'type', render: (text, record, index) => { @@ -188,6 +275,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.STATUS, title: t('状态'), dataIndex: 'status', render: (text, record, index) => { @@ -211,6 +299,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.RESPONSE_TIME, title: t('响应时间'), dataIndex: 'response_time', render: (text, record, index) => { @@ -218,6 +307,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.BALANCE, title: t('已用/剩余'), dataIndex: 'expired_time', render: (text, record, index) => { @@ -255,6 +345,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.PRIORITY, title: t('优先级'), dataIndex: 'priority', render: (text, record, index) => { @@ -304,6 +395,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.WEIGHT, title: t('权重'), dataIndex: 'weight', render: (text, record, index) => { @@ -353,6 +445,7 @@ const ChannelsTable = () => { } }, { + key: COLUMN_KEYS.OPERATE, title: '', dataIndex: 'operate', render: (text, record, index) => { @@ -493,6 +586,68 @@ const ChannelsTable = () => { } ]; + // Filter columns based on visibility settings + const getVisibleColumns = () => { + return allColumns.filter(column => visibleColumns[column.key]); + }; + + // Column selector modal + const renderColumnSelector = () => { + return ( + setShowColumnSelector(false)} + footer={ + <> + + + + + } + style={{ width: 500 }} + bodyStyle={{ padding: '24px' }} + > +
+ v === true)} + indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)} + onChange={e => handleSelectAll(e.target.checked)} + > + {t('全选')} + +
+
+ {allColumns.map(column => { + // Skip columns without title + if (!column.title) { + return null; + } + + return ( +
+ handleColumnVisibilityChange(column.key, e.target.checked)} + > + {column.title} + +
+ ); + })} +
+
+ ); + }; + const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -1032,6 +1187,7 @@ const ChannelsTable = () => { return ( <> + {renderColumnSelector()} { > {t('批量设置标签')} + - { }, onPageChange: handlePageChange }} - loading={loading} onRow={handleRow} rowSelection={ enableBatchDelete diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index abf28297e..b2fa24a2c 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -21,7 +21,8 @@ import { Spin, Table, Tag, - Tooltip + Tooltip, + Checkbox } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; import { @@ -34,7 +35,7 @@ import { import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; import { getLogOther } from '../helpers/other.js'; import { StyleContext } from '../context/Style/index.js'; -import { IconInherit, IconRefresh } from '@douyinfe/semi-icons'; +import { IconInherit, IconRefresh, IconSetting } from '@douyinfe/semi-icons'; const { Header } = Layout; @@ -215,12 +216,104 @@ const LogsTable = () => { } - const columns = [ + // Define column keys for selection + const COLUMN_KEYS = { + TIME: 'time', + CHANNEL: 'channel', + USERNAME: 'username', + TOKEN: 'token', + GROUP: 'group', + TYPE: 'type', + MODEL: 'model', + USE_TIME: 'use_time', + PROMPT: 'prompt', + COMPLETION: 'completion', + COST: 'cost', + RETRY: 'retry', + DETAILS: 'details' + }; + + // State for column visibility + const [visibleColumns, setVisibleColumns] = useState({}); + const [showColumnSelector, setShowColumnSelector] = useState(false); + + // Load saved column preferences from localStorage + useEffect(() => { + const savedColumns = localStorage.getItem('logs-table-columns'); + if (savedColumns) { + try { + const parsed = JSON.parse(savedColumns); + // Make sure all columns are accounted for + const defaults = getDefaultColumnVisibility(); + const merged = { ...defaults, ...parsed }; + setVisibleColumns(merged); + } catch (e) { + console.error('Failed to parse saved column preferences', e); + initDefaultColumns(); + } + } else { + initDefaultColumns(); + } + }, []); + + // Get default column visibility based on user role + const getDefaultColumnVisibility = () => { + return { + [COLUMN_KEYS.TIME]: true, + [COLUMN_KEYS.CHANNEL]: isAdminUser, + [COLUMN_KEYS.USERNAME]: isAdminUser, + [COLUMN_KEYS.TOKEN]: true, + [COLUMN_KEYS.GROUP]: true, + [COLUMN_KEYS.TYPE]: true, + [COLUMN_KEYS.MODEL]: true, + [COLUMN_KEYS.USE_TIME]: true, + [COLUMN_KEYS.PROMPT]: true, + [COLUMN_KEYS.COMPLETION]: true, + [COLUMN_KEYS.COST]: true, + [COLUMN_KEYS.RETRY]: isAdminUser, + [COLUMN_KEYS.DETAILS]: true + }; + }; + + // Initialize default column visibility + const initDefaultColumns = () => { + const defaults = getDefaultColumnVisibility(); + setVisibleColumns(defaults); + localStorage.setItem('logs-table-columns', JSON.stringify(defaults)); + }; + + // Handle column visibility change + const handleColumnVisibilityChange = (columnKey, checked) => { + const updatedColumns = { ...visibleColumns, [columnKey]: checked }; + setVisibleColumns(updatedColumns); + }; + + // Handle "Select All" checkbox + const handleSelectAll = (checked) => { + const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]); + const updatedColumns = {}; + + allKeys.forEach(key => { + // For admin-only columns, only enable them if user is admin + if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) { + updatedColumns[key] = false; + } else { + updatedColumns[key] = checked; + } + }); + + setVisibleColumns(updatedColumns); + }; + + // Define all columns + const allColumns = [ { + key: COLUMN_KEYS.TIME, title: t('时间'), dataIndex: 'timestamp2string', }, { + key: COLUMN_KEYS.CHANNEL, title: t('渠道'), dataIndex: 'channel', className: isAdmin() ? 'tableShow' : 'tableHiddle', @@ -249,6 +342,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.USERNAME, title: t('用户'), dataIndex: 'username', className: isAdmin() ? 'tableShow' : 'tableHiddle', @@ -274,6 +368,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.TOKEN, title: t('令牌'), dataIndex: 'token_name', render: (text, record, index) => { @@ -297,6 +392,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.GROUP, title: t('分组'), dataIndex: 'group', render: (text, record, index) => { @@ -333,6 +429,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.TYPE, title: t('类型'), dataIndex: 'type', render: (text, record, index) => { @@ -340,6 +437,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.MODEL, title: t('模型'), dataIndex: 'model_name', render: (text, record, index) => { @@ -351,6 +449,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.USE_TIME, title: t('用时/首字'), dataIndex: 'use_time', render: (text, record, index) => { @@ -378,6 +477,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.PROMPT, title: t('提示'), dataIndex: 'prompt_tokens', render: (text, record, index) => { @@ -389,6 +489,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.COMPLETION, title: t('补全'), dataIndex: 'completion_tokens', render: (text, record, index) => { @@ -401,6 +502,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.COST, title: t('花费'), dataIndex: 'quota', render: (text, record, index) => { @@ -412,6 +514,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.RETRY, title: t('重试'), dataIndex: 'retry', className: isAdmin() ? 'tableShow' : 'tableHiddle', @@ -439,6 +542,7 @@ const LogsTable = () => { }, }, { + key: COLUMN_KEYS.DETAILS, title: t('详情'), dataIndex: 'content', render: (text, record, index) => { @@ -481,6 +585,76 @@ const LogsTable = () => { }, ]; + // Update table when column visibility changes + useEffect(() => { + if (Object.keys(visibleColumns).length > 0) { + // Save to localStorage + localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns)); + } + }, [visibleColumns]); + + // Filter columns based on visibility settings + const getVisibleColumns = () => { + return allColumns.filter(column => visibleColumns[column.key]); + }; + + // Column selector modal + const renderColumnSelector = () => { + return ( + setShowColumnSelector(false)} + footer={ + <> + + + + + } + > +
+ v === true)} + indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)} + onChange={e => handleSelectAll(e.target.checked)} + > + {t('全选')} + +
+
+ {allColumns.map(column => { + // Skip admin-only columns for non-admin users + if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL || + column.key === COLUMN_KEYS.USERNAME || + column.key === COLUMN_KEYS.RETRY)) { + return null; + } + + return ( +
+ handleColumnVisibilityChange(column.key, e.target.checked)} + > + {column.title} + +
+ ); + })} +
+
+ ); + }; + const [styleState, styleDispatch] = useContext(StyleContext); const [logs, setLogs] = useState([]); const [expandData, setExpandData] = useState({}); @@ -782,8 +956,9 @@ const LogsTable = () => { return ( <> + {renderColumnSelector()} -
+
@@ -917,10 +1092,19 @@ const LogsTable = () => { {t('管理')} {t('系统')} +