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