From 0401f1e9ecb879e5997a2f3b7934c87c596d8f82 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Fri, 13 Jun 2025 01:34:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20feat:=20Add=20user-configurable?= =?UTF-8?q?=20IP=20logging=20for=20consume=20and=20error=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IP field to Log model with database index and default empty value - Implement conditional IP recording based on user setting in RecordConsumeLog and RecordErrorLog - Add UserSettingRecordIpLog constant and update user settings API to handle record_ip_log field - Create dedicated "IP记录" tab in personal settings under "其他设置" section - Add IP column to logs table with help tooltip explaining recording conditions - Make IP column visible to all users (not admin-only) with proper filtering for consume/error log types - Restrict display of use_time and retry columns to consume and error log types only - Update personal settings UI structure: rename "通知设置" to "其他设置" to accommodate new functionality - Add proper translation support and maintain consistent styling across components The IP logging feature is disabled by default and only records client IP addresses for consume (type 2) and error (type 5) logs when explicitly enabled by users in their personal settings. --- constant/user_setting.go | 1 + controller/user.go | 2 + model/log.go | 22 +++++++ .../components/settings/PersonalSetting.js | 57 ++++++++++++++++--- web/src/components/table/LogsTable.js | 40 ++++++++++++- web/src/i18n/locales/en.json | 6 +- 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/constant/user_setting.go b/constant/user_setting.go index 055884f7..7e79035e 100644 --- a/constant/user_setting.go +++ b/constant/user_setting.go @@ -7,6 +7,7 @@ var ( UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥 UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址 UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型 + UserSettingRecordIpLog = "record_ip_log" // 是否记录请求和错误日志IP ) var ( diff --git a/controller/user.go b/controller/user.go index fd53e743..d7eb42d7 100644 --- a/controller/user.go +++ b/controller/user.go @@ -943,6 +943,7 @@ type UpdateUserSettingRequest struct { WebhookSecret string `json:"webhook_secret,omitempty"` NotificationEmail string `json:"notification_email,omitempty"` AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"` + RecordIpLog bool `json:"record_ip_log"` } func UpdateUserSetting(c *gin.Context) { @@ -1019,6 +1020,7 @@ func UpdateUserSetting(c *gin.Context) { constant.UserSettingNotifyType: req.QuotaWarningType, constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold, "accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel, + constant.UserSettingRecordIpLog: req.RecordIpLog, } // 如果是webhook类型,添加webhook相关设置 diff --git a/model/log.go b/model/log.go index 0a891fcd..3df961e1 100644 --- a/model/log.go +++ b/model/log.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "one-api/common" + "one-api/constant" "os" "strings" "time" @@ -32,6 +33,7 @@ type Log struct { ChannelName string `json:"channel_name" gorm:"->"` TokenId int `json:"token_id" gorm:"default:0;index"` Group string `json:"group" gorm:"index"` + Ip string `json:"ip" gorm:"index;default:''"` Other string `json:"other"` } @@ -95,6 +97,15 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, common.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content)) username := c.GetString("username") otherStr := common.MapToJsonStr(other) + // 判断是否需要记录 IP + needRecordIp := false + if settingMap, err := GetUserSetting(userId, false); err == nil { + if v, ok := settingMap[constant.UserSettingRecordIpLog]; ok { + if vb, ok := v.(bool); ok && vb { + needRecordIp = true + } + } + } log := &Log{ UserId: userId, Username: username, @@ -111,6 +122,7 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, UseTime: useTimeSeconds, IsStream: isStream, Group: group, + Ip: func() string { if needRecordIp { return c.ClientIP() }; return "" }(), Other: otherStr, } err := LOG_DB.Create(log).Error @@ -128,6 +140,15 @@ func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens in } username := c.GetString("username") otherStr := common.MapToJsonStr(other) + // 判断是否需要记录 IP + needRecordIp := false + if settingMap, err := GetUserSetting(userId, false); err == nil { + if v, ok := settingMap[constant.UserSettingRecordIpLog]; ok { + if vb, ok := v.(bool); ok && vb { + needRecordIp = true + } + } + } log := &Log{ UserId: userId, Username: username, @@ -144,6 +165,7 @@ func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens in UseTime: useTimeSeconds, IsStream: isStream, Group: group, + Ip: func() string { if needRecordIp { return c.ClientIP() }; return "" }(), Other: otherStr, } err := LOG_DB.Create(log).Error diff --git a/web/src/components/settings/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js index 3228d184..95821332 100644 --- a/web/src/components/settings/PersonalSetting.js +++ b/web/src/components/settings/PersonalSetting.js @@ -103,6 +103,7 @@ const PersonalSetting = () => { webhookSecret: '', notificationEmail: '', acceptUnsetModelRatioModel: false, + recordIpLog: false, }); const [modelsLoading, setModelsLoading] = useState(true); const [showWebhookDocs, setShowWebhookDocs] = useState(true); @@ -147,6 +148,7 @@ const PersonalSetting = () => { notificationEmail: settings.notification_email || '', acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, + recordIpLog: settings.record_ip_log || false, }); } }, [userState?.user?.setting]); @@ -346,7 +348,7 @@ const PersonalSetting = () => { const handleNotificationSettingChange = (type, value) => { setNotificationSettings((prev) => ({ ...prev, - [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象 + [type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly })); }; @@ -362,6 +364,7 @@ const PersonalSetting = () => { notification_email: notificationSettings.notificationEmail, accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel, + record_ip_log: notificationSettings.recordIpLog, }); if (res.data.success) { @@ -1063,7 +1066,7 @@ const PersonalSetting = () => { tab={
- {t('通知设置')} + {t('其他设置')}
} itemKey='notification' @@ -1228,28 +1231,68 @@ const PersonalSetting = () => { +
+
+ {/* 接受未设置价格模型 */} +
+
+
+ +
+
+
+
+ + {t('接受未设置价格模型')} + +
+ {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} +
+
+ + handleNotificationSettingChange( + 'acceptUnsetModelRatioModel', + e.target.checked, + ) + } + className="ml-4" + /> +
+
+
+
+
+
+
+ +
- +
- {t('接受未设置价格模型')} + {t('记录请求与错误日志 IP')}
- {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} + {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')}
handleNotificationSettingChange( - 'acceptUnsetModelRatioModel', + 'recordIpLog', e.target.checked, ) } diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js index 6c8996a0..27c34a4d 100644 --- a/web/src/components/table/LogsTable.js +++ b/web/src/components/table/LogsTable.js @@ -47,7 +47,7 @@ import { } from '@douyinfe/semi-illustrations'; import { ITEMS_PER_PAGE } from '../../constants'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; -import { IconSetting, IconSearch } from '@douyinfe/semi-icons'; +import { IconSetting, IconSearch, IconHelpCircle } from '@douyinfe/semi-icons'; import { Route } from 'lucide-react'; const { Text } = Typography; @@ -260,6 +260,7 @@ const LogsTable = () => { COMPLETION: 'completion', COST: 'cost', RETRY: 'retry', + IP: 'ip', DETAILS: 'details', }; @@ -301,6 +302,7 @@ const LogsTable = () => { [COLUMN_KEYS.COMPLETION]: true, [COLUMN_KEYS.COST]: true, [COLUMN_KEYS.RETRY]: isAdminUser, + [COLUMN_KEYS.IP]: true, [COLUMN_KEYS.DETAILS]: true, }; }; @@ -485,6 +487,9 @@ const LogsTable = () => { title: t('用时/首字'), dataIndex: 'use_time', render: (text, record, index) => { + if (!(record.type === 2 || record.type === 5)) { + return <>; + } if (record.is_stream) { let other = getLogOther(record.other); return ( @@ -545,12 +550,45 @@ const LogsTable = () => { ); }, }, + { + key: COLUMN_KEYS.IP, + title: ( +
+ {t('IP')} + + + +
+ ), + dataIndex: 'ip', + render: (text, record, index) => { + return (record.type === 2 || record.type === 5) && text ? ( + + { + copyText(event, text); + }} + > + {text} + + + ) : ( + <> + ); + }, + }, { key: COLUMN_KEYS.RETRY, title: t('重试'), dataIndex: 'retry', className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { + if (!(record.type === 2 || record.type === 5)) { + return <>; + } let content = t('渠道') + `:${record.channel}`; if (record.other !== '') { let other = JSON.parse(record.other); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index f4bede2c..6b0d80fb 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1646,5 +1646,9 @@ "高延迟": "High latency", "维护中": "Maintenance", "暂无监控数据": "No monitoring data", - "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings." + "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.", + "IP记录": "IP Record", + "记录请求与错误日志 IP": "Record request and error log IP", + "开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address", + "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed" } \ No newline at end of file