From b026f6bc042ea8ee291b306a8b6b0d4f99e82bfc Mon Sep 17 00:00:00 2001 From: fengsilin Date: Thu, 7 May 2026 17:47:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E5=80=8D=E7=8E=87?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA=20+=20=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E9=80=89=E6=8B=A9=20+=20=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserRatioSection 渠道 ID 输入改为带搜索的下拉选择器 - 使用日志详情、计费过程、展开行均显示用户倍率 - renderLogContent suffix 统一追加,消除 4 处重复 - 提取 dumpUpstreamRequest 辅助函数,debug 日志走 SysLog Co-Authored-By: Claude --- relay/channel/api_request.go | 22 ++-- service/log_info_generate.go | 3 + .../table/users/modals/UserRatioSection.jsx | 43 ++++++- web/src/helpers/render.jsx | 106 ++++++++++-------- web/src/hooks/usage-logs/useUsageLogsData.jsx | 9 ++ 5 files changed, 124 insertions(+), 59 deletions(-) diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 43def3e..7b502c2 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "bytes" "io" "net/http" "regexp" @@ -293,13 +294,12 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody if err != nil { return nil, fmt.Errorf("setup request header failed: %w", err) } - // 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高 - // 这样可以覆盖默认的 Authorization header 设置 headerOverride, err := processHeaderOverride(info, c) if err != nil { return nil, err } applyHeaderOverrideToRequest(req, headerOverride) + dumpUpstreamRequest(req) resp, err := doRequest(c, req, info) if err != nil { return nil, fmt.Errorf("do request failed: %w", err) @@ -319,20 +319,18 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod if err != nil { return nil, fmt.Errorf("new request failed: %w", err) } - // set form data req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) headers := req.Header err = a.SetupRequestHeader(c, &headers, info) if err != nil { return nil, fmt.Errorf("setup request header failed: %w", err) } - // 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高 - // 这样可以覆盖默认的 Authorization header 设置 headerOverride, err := processHeaderOverride(info, c) if err != nil { return nil, err } applyHeaderOverrideToRequest(req, headerOverride) + dumpUpstreamRequest(req) resp, err := doRequest(c, req, info) if err != nil { return nil, fmt.Errorf("do request failed: %w", err) @@ -340,6 +338,15 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod return resp, nil } +func dumpUpstreamRequest(req *http.Request) { + if !common2.DebugEnabled || req == nil { + return + } + bodyBytes, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + common2.SysLog(fmt.Sprintf("[UpstreamRequest] URL: %s\nHeaders: %v\nBody: %s", req.URL.String(), req.Header, string(bodyBytes))) +} + func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*websocket.Conn, error) { fullRequestURL, err := a.GetRequestURL(info) if err != nil { @@ -347,11 +354,6 @@ func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody } targetHeader := http.Header{} err = a.SetupRequestHeader(c, &targetHeader, info) - if err != nil { - return nil, fmt.Errorf("setup request header failed: %w", err) - } - // 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高 - // 这样可以覆盖默认的 Authorization header 设置 headerOverride, err := processHeaderOverride(info, c) if err != nil { return nil, err diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 1c44091..46cca48 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -41,6 +41,9 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m other["cache_ratio"] = cacheRatio other["model_price"] = modelPrice other["user_group_ratio"] = userGroupRatio + if relayInfo != nil && relayInfo.PriceData.UserChannelRatio != 1.0 { + other["user_channel_ratio"] = relayInfo.PriceData.UserChannelRatio + } other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli()) if relayInfo.ReasoningEffort != "" { other["reasoning_effort"] = relayInfo.ReasoningEffort diff --git a/web/src/components/table/users/modals/UserRatioSection.jsx b/web/src/components/table/users/modals/UserRatioSection.jsx index d404d49..2e9e3de 100644 --- a/web/src/components/table/users/modals/UserRatioSection.jsx +++ b/web/src/components/table/users/modals/UserRatioSection.jsx @@ -20,6 +20,7 @@ const UserRatioSection = ({ userId }) => { const [ratios, setRatios] = useState([]); const [loading, setLoading] = useState(false); const [addModalVisible, setAddModalVisible] = useState(false); + const [channelOptions, setChannelOptions] = useState([]); const formApiRef = useRef(null); const loadRatios = async () => { @@ -39,8 +40,29 @@ const UserRatioSection = ({ userId }) => { setLoading(false); }; + const loadChannels = async () => { + try { + const res = await API.get('/api/channel/?p=0&page_size=500'); + const { success, data, message } = res.data; + if (success) { + const options = (data?.items || []).map((ch) => ({ + label: ch.remark + ? `${ch.name} (${ch.remark})` + : `${ch.name} (ID: ${ch.id})`, + value: ch.id, + })); + setChannelOptions(options); + } else { + showError(message); + } + } catch (e) { + showError(e.message); + } + }; + useEffect(() => { loadRatios(); + loadChannels(); }, [userId]); const handleAdd = async (values) => { @@ -86,7 +108,15 @@ const UserRatioSection = ({ userId }) => { const columns = [ { title: t('模型'), dataIndex: 'model_name', key: 'model_name' }, - { title: t('渠道 ID'), dataIndex: 'channel_id', key: 'channel_id' }, + { + title: t('渠道'), + dataIndex: 'channel_id', + key: 'channel_id', + render: (channelId) => { + const ch = channelOptions.find((c) => c.value === channelId); + return ch ? ch.label : channelId; + }, + }, { title: t('倍率'), dataIndex: 'ratio', @@ -166,11 +196,14 @@ const UserRatioSection = ({ userId }) => { placeholder='gpt-4o' rules={[{ required: true, message: t('请输入模型名称') }]} /> -
@@ -1490,13 +1497,14 @@ export function renderModelPrice( // 构建输出部分描述 const outputDesc = i18next.t( - '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', + '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}{{userRatio}}', { completion: completionTokens, symbol: symbol, compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, + userRatio: ucr ? ` * 用户倍率 ${ucr}` : '', }, ); @@ -1571,6 +1579,7 @@ export function renderLogContent( webSearchCallCount = 0, fileSearch = false, fileSearchCallCount = 0, + userChannelRatio, ) { const { ratio, @@ -1581,51 +1590,55 @@ export function renderLogContent( // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); + const userRatioSuffix = (userChannelRatio != null && userChannelRatio !== 1.0) + ? i18next.t(',用户倍率 {{userChannelRatio}}', { userChannelRatio }) + : ''; + + let result; if (modelPrice !== -1) { - return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { + result = i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { symbol: symbol, price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio, }); + } else if (image) { + result = i18next.t( + '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + cacheRatio: cacheRatio, + completionRatio: completionRatio, + imageRatio: imageRatio, + ratioType: ratioLabel, + ratio, + }, + ); + } else if (webSearch) { + result = i18next.t( + '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次', + { + modelRatio: modelRatio, + cacheRatio: cacheRatio, + completionRatio: completionRatio, + ratioType: ratioLabel, + ratio, + webSearchCallCount, + }, + ); } else { - if (image) { - return i18next.t( - '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', - { - modelRatio: modelRatio, - cacheRatio: cacheRatio, - completionRatio: completionRatio, - imageRatio: imageRatio, - ratioType: ratioLabel, - ratio, - }, - ); - } else if (webSearch) { - return i18next.t( - '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次', - { - modelRatio: modelRatio, - cacheRatio: cacheRatio, - completionRatio: completionRatio, - ratioType: ratioLabel, - ratio, - webSearchCallCount, - }, - ); - } else { - return i18next.t( - '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', - { - modelRatio: modelRatio, - cacheRatio: cacheRatio, - completionRatio: completionRatio, - ratioType: ratioLabel, - ratio, - }, - ); - } + result = i18next.t( + '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + cacheRatio: cacheRatio, + completionRatio: completionRatio, + ratioType: ratioLabel, + ratio, + }, + ); } + return result + userRatioSuffix; } export function renderModelPriceSimple( @@ -2148,6 +2161,7 @@ export function renderClaudeLogContent( cacheCreationRatio5m = 1.0, cacheCreationTokens1h = 0, cacheCreationRatio1h = 1.0, + userChannelRatio, ) { const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( groupRatio, @@ -2158,13 +2172,17 @@ export function renderClaudeLogContent( // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); + const userRatioSuffix = (userChannelRatio != null && userChannelRatio !== 1.0) + ? i18next.t(',用户倍率 {{userChannelRatio}}', { userChannelRatio }) + : ''; + if (modelPrice !== -1) { return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { symbol: symbol, price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio: groupRatio, - }); + }) + userRatioSuffix; } else { const hasSplitCacheCreation = cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; @@ -2217,7 +2235,7 @@ export function renderClaudeLogContent( }), ]; - return parts.join(','); + return parts.join(',') + userRatioSuffix; } } diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index 51bbeef..88786d4 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -422,6 +422,7 @@ export const useLogsData = () => { other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0, + other?.user_channel_ratio, ) : renderLogContent( other?.model_ratio, @@ -436,6 +437,7 @@ export const useLogsData = () => { other.web_search_call_count || 0, other.file_search || false, other.file_search_call_count || 0, + other?.user_channel_ratio, ), }); if (logs[i]?.content) { @@ -537,6 +539,7 @@ export const useLogsData = () => { other?.audio_input_price || 0, other?.image_generation_call || false, other?.image_generation_call_price || 0, + other?.user_channel_ratio, ); } expandDataLocal.push({ @@ -550,6 +553,12 @@ export const useLogsData = () => { value: other.reasoning_effort, }); } + if (other?.user_channel_ratio) { + expandDataLocal.push({ + key: t('用户倍率'), + value: other.user_channel_ratio, + }); + } } if (logs[i].type === 6) { if (other?.task_id) {