Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b9246f788 | |||
| f8a8dbec6e | |||
| 47a92439fc | |||
| 69e6d5ce51 |
@@ -65,6 +65,14 @@
|
||||
- Add suffix `-low` to set low reasoning effort
|
||||
17. 🔄 Thinking to content option `thinking_to_content` in `Channel->Edit->Channel Extra Settings`, default is `false`, when `true`, the `reasoning_content` of the thinking content will be converted to `<think>` tags and concatenated to the content returned.
|
||||
18. 🔄 Model rate limit, support setting total request limit and successful request limit in `System Settings->Rate Limit Settings`
|
||||
19. 💰 Cache billing support, when enabled can charge a configurable ratio for cache hits:
|
||||
1. Set `Prompt Cache Ratio` in `System Settings -> Operation Settings`
|
||||
2. Set `Prompt Cache Ratio` in channel settings, range 0-1 (e.g., 0.5 means 50% charge on cache hits)
|
||||
3. Supported channels:
|
||||
- [x] OpenAI
|
||||
- [x] Azure
|
||||
- [x] DeepSeek
|
||||
- [ ] Claude
|
||||
|
||||
## Model Support
|
||||
This version additionally supports:
|
||||
|
||||
@@ -74,6 +74,14 @@
|
||||
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
|
||||
18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回。
|
||||
19. 🔄 模型限流,支持在 `系统设置-速率限制设置` 中设置模型限流,支持设置总请求数限制和成功请求数限制
|
||||
20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
|
||||
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
|
||||
3. 支持的渠道:
|
||||
- [x] OpenAI
|
||||
- [x] Azure
|
||||
- [x] DeepSeek
|
||||
- [ ] Claude
|
||||
|
||||
## 模型支持
|
||||
此版本额外支持以下模型:
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 (
|
||||
<Modal
|
||||
title={t('列设置')}
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||
<Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
|
||||
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
|
||||
</>
|
||||
}
|
||||
style={{ width: 500 }}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every(v => v === true)}
|
||||
indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
|
||||
onChange={e => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
{t('全选')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{allColumns.map(column => {
|
||||
// Skip columns without title
|
||||
if (!column.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
|
||||
>
|
||||
{column.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@@ -1032,6 +1187,7 @@ const ChannelsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<EditTagModal
|
||||
visible={showEditTag}
|
||||
tag={editingTag}
|
||||
@@ -1238,15 +1394,22 @@ const ChannelsTable = () => {
|
||||
>
|
||||
{t('批量设置标签')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Table
|
||||
className={'channel-table'}
|
||||
style={{ marginTop: 15 }}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
columns={getVisibleColumns()}
|
||||
dataSource={pageData}
|
||||
pagination={{
|
||||
currentPage: activePage,
|
||||
@@ -1260,7 +1423,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
onPageChange: handlePageChange
|
||||
}}
|
||||
loading={loading}
|
||||
onRow={handleRow}
|
||||
rowSelection={
|
||||
enableBatchDelete
|
||||
|
||||
@@ -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 (
|
||||
<Modal
|
||||
title={t('列设置')}
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||
<Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
|
||||
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every(v => v === true)}
|
||||
indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
|
||||
onChange={e => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
{t('全选')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{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 (
|
||||
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
|
||||
>
|
||||
{column.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [expandData, setExpandData] = useState({});
|
||||
@@ -782,8 +956,9 @@ const LogsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<Layout>
|
||||
<Header>
|
||||
<Header style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
|
||||
<Spin spinning={loadingStat}>
|
||||
<Space>
|
||||
<Tag color='green' size='large' style={{ padding: 15 }}>
|
||||
@@ -917,10 +1092,19 @@ const LogsTable = () => {
|
||||
<Select.Option value='3'>{t('管理')}</Select.Option>
|
||||
<Select.Option value='4'>{t('系统')}</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
style={{ marginTop: 5 }}
|
||||
columns={columns}
|
||||
columns={getVisibleColumns()}
|
||||
expandedRowRender={expandRowRender}
|
||||
expandRowByClick={true}
|
||||
dataSource={logs}
|
||||
|
||||
@@ -294,39 +294,36 @@ const SiderBar = () => {
|
||||
}}
|
||||
>
|
||||
{/* Chat Section - Only show if there are chat items */}
|
||||
{chatItems.length > 0 && (
|
||||
<>
|
||||
{chatMenuItems.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.items.map((subItem) => (
|
||||
<Nav.Item
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={subItem.text}
|
||||
/>
|
||||
))}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
{chatMenuItems.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.items.map((subItem) => (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={subItem.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<Divider style={dividerStyle} />
|
||||
|
||||
@@ -1338,5 +1338,9 @@
|
||||
"0.1-1之间的小数": "Decimal between 0.1 and 1",
|
||||
"模型相关设置": "Model related settings",
|
||||
"收起侧边栏": "Collapse sidebar",
|
||||
"展开侧边栏": "Expand sidebar"
|
||||
"展开侧边栏": "Expand sidebar",
|
||||
"提示缓存倍率": "Prompt cache ratio",
|
||||
"缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})",
|
||||
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
|
||||
"缓存 Tokens": "Cache Tokens"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user