Browse Source

update1.2

aiprodcoder 3 months ago
parent
commit
f1e087696b
12 changed files with 478 additions and 37 deletions
  1. 1 0
      VERSION
  2. 3 2
      common/constants.go
  3. 97 0
      controller/channel_export.go
  4. 216 0
      controller/channel_import.go
  5. 19 4
      controller/log.go
  6. 12 6
      go.mod
  7. 27 13
      go.sum
  8. 1 1
      main.go
  9. 35 0
      model/log.go
  10. 3 0
      model/option.go
  11. 62 11
      relay/relay-text.go
  12. 2 0
      router/api-router.go

+ 1 - 0
VERSION

@@ -0,0 +1 @@
+v1.2

+ 3 - 2
common/constants.go

@@ -18,8 +18,8 @@ var TopUpLink = ""
 
 // var ChatLink = ""
 // var ChatLink2 = ""
-var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
-var DisplayInCurrencyEnabled = true
+var QuotaPerUnit = 500 * 1000.0      // $0.002 / 1K tokens
+var DisplayInCurrencyEnabled = false // true
 var DisplayTokenStatEnabled = true
 var DrawingEnabled = true
 var TaskEnabled = true
@@ -71,6 +71,7 @@ var DebugEnabled bool
 var MemoryCacheEnabled bool
 
 var LogConsumeEnabled = true
+var LogUserInputEnabled = true
 
 var SMTPServer = ""
 var SMTPPort = 587

+ 97 - 0
controller/channel_export.go

@@ -0,0 +1,97 @@
+package controller
+
+import (
+	"bytes"
+	"net/http"
+	"one-api/common"
+	"one-api/model"
+
+	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
+)
+
+func ExportChannels(c *gin.Context) {
+	// 获取所有渠道数据
+	channels, err := model.GetAllChannels(0, 0, true, false)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	// 创建Excel文件
+	f := excelize.NewFile()
+	defer func() {
+		if err := f.Close(); err != nil {
+			common.SysError("Error closing Excel file: " + err.Error())
+		}
+	}()
+
+	// 创建工作表
+	sheetName := "Channels"
+	f.SetSheetName("Sheet1", sheetName)
+
+	// 设置表头
+	headers := []string{
+		"ID", "名称", "类型", "状态", "密钥", "组织", "测试模型",
+		"权重", "创建时间", "测试时间", "响应时间", "基础URL", "其他",
+		"余额", "余额更新时间", "模型", "分组", "已用配额",
+		"模型映射", "状态码映射", "优先级", "自动禁用", "标签", "额外设置", "参数覆盖",
+	}
+
+	for i, header := range headers {
+		cell, _ := excelize.CoordinatesToCellName(i+1, 1)
+		f.SetCellValue(sheetName, cell, header)
+	}
+
+	// 填充数据
+	for i, channel := range channels {
+		row := i + 2 // 从第二行开始填充数据
+		data := []interface{}{
+			channel.Id,
+			channel.Name,
+			channel.Type,
+			channel.Status,
+			channel.Key,
+			channel.OpenAIOrganization,
+			channel.TestModel,
+			channel.Weight,
+			channel.CreatedTime,
+			channel.TestTime,
+			channel.ResponseTime,
+			channel.BaseURL,
+			channel.Other,
+			channel.Balance,
+			channel.BalanceUpdatedTime,
+			channel.Models,
+			channel.Group,
+			channel.UsedQuota,
+			channel.ModelMapping,
+			channel.StatusCodeMapping,
+			channel.Priority,
+			channel.AutoBan,
+			channel.Tag,
+			channel.Setting,
+			channel.ParamOverride,
+		}
+
+		for j, value := range data {
+			cell, _ := excelize.CoordinatesToCellName(j+1, row)
+			f.SetCellValue(sheetName, cell, value)
+		}
+	}
+
+	// 设置响应头
+	c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+	c.Header("Content-Disposition", "attachment; filename=channels.xlsx")
+	c.Header("Cache-Control", "no-cache")
+
+	// 将Excel文件写入缓冲区
+	var buf bytes.Buffer
+	if err := f.Write(&buf); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	// 返回Excel文件
+	c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
+}

+ 216 - 0
controller/channel_import.go

@@ -0,0 +1,216 @@
+package controller
+
+import (
+	"fmt"
+	"net/http"
+	"one-api/common"
+	"one-api/model"
+	"strconv"
+
+	// "strings"
+
+	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
+)
+
+func ImportChannels(c *gin.Context) {
+	// 从请求中获取上传的文件
+	file, err := c.FormFile("file")
+	if err != nil {
+		common.ApiError(c, fmt.Errorf("failed to get file: %w", err))
+		return
+	}
+
+	// 打开文件
+	src, err := file.Open()
+	if err != nil {
+		common.ApiError(c, fmt.Errorf("failed to open file: %w", err))
+		return
+	}
+	defer src.Close()
+
+	// 读取Excel文件
+	f, err := excelize.OpenReader(src)
+	if err != nil {
+		common.ApiError(c, fmt.Errorf("failed to open Excel file: %w", err))
+		return
+	}
+	defer func() {
+		if err := f.Close(); err != nil {
+			common.SysError("Error closing Excel file: " + err.Error())
+		}
+	}()
+
+	// 获取第一个工作表
+	sheetName := f.GetSheetName(0)
+	if sheetName == "" {
+		common.ApiError(c, fmt.Errorf("no sheets found in Excel file"))
+		return
+	}
+
+	// 读取所有行
+	rows, err := f.GetRows(sheetName)
+	if err != nil {
+		common.ApiError(c, fmt.Errorf("failed to read rows: %w", err))
+		return
+	}
+
+	// 检查是否有数据
+	if len(rows) <= 1 {
+		common.ApiError(c, fmt.Errorf("no data found in Excel file"))
+		return
+	}
+
+	// 解析表头
+	headers := rows[0]
+	headerMap := make(map[string]int)
+	for i, header := range headers {
+		headerMap[header] = i
+	}
+
+	// 解析数据行
+	channels := make([]model.Channel, 0, len(rows)-1)
+	for i := 1; i < len(rows); i++ {
+		row := rows[i]
+		if len(row) == 0 {
+			continue
+		}
+
+		// 创建渠道对象
+		channel := model.Channel{}
+
+		// 根据表头映射填充数据
+		for header, colIndex := range headerMap {
+			if colIndex >= len(row) {
+				continue
+			}
+			value := row[colIndex]
+
+			switch header {
+			case "ID":
+				// ID由数据库自动生成,不需要设置
+			case "名称":
+				channel.Name = value
+			case "类型":
+				if v, err := strconv.Atoi(value); err == nil {
+					channel.Type = v
+				}
+			case "状态":
+				if v, err := strconv.Atoi(value); err == nil {
+					channel.Status = v
+				}
+			case "密钥":
+				channel.Key = value
+			case "组织":
+				if value != "" {
+					channel.OpenAIOrganization = &value
+				}
+			case "测试模型":
+				if value != "" {
+					channel.TestModel = &value
+				}
+			case "权重":
+				if value != "" {
+					if v, err := strconv.ParseUint(value, 10, 32); err == nil {
+						weight := uint(v)
+						channel.Weight = &weight
+					}
+				}
+			case "创建时间":
+				if v, err := strconv.ParseInt(value, 10, 64); err == nil {
+					channel.CreatedTime = v
+				}
+			case "测试时间":
+				if v, err := strconv.ParseInt(value, 10, 64); err == nil {
+					channel.TestTime = v
+				}
+			case "响应时间":
+				if v, err := strconv.Atoi(value); err == nil {
+					channel.ResponseTime = v
+				}
+			case "基础URL":
+				if value != "" {
+					channel.BaseURL = &value
+				}
+			case "其他":
+				channel.Other = value
+			case "余额":
+				if v, err := strconv.ParseFloat(value, 64); err == nil {
+					channel.Balance = v
+				}
+			case "余额更新时间":
+				if v, err := strconv.ParseInt(value, 10, 64); err == nil {
+					channel.BalanceUpdatedTime = v
+				}
+			case "模型":
+				channel.Models = value
+			case "分组":
+				channel.Group = value
+			case "已用配额":
+				if v, err := strconv.ParseInt(value, 10, 64); err == nil {
+					channel.UsedQuota = v
+				}
+			case "模型映射":
+				if value != "" {
+					channel.ModelMapping = &value
+				}
+			case "状态码映射":
+				if value != "" {
+					channel.StatusCodeMapping = &value
+				}
+			case "优先级":
+				if value != "" {
+					if v, err := strconv.ParseInt(value, 10, 64); err == nil {
+						channel.Priority = &v
+					}
+				}
+			case "自动禁用":
+				if value != "" {
+					if v, err := strconv.Atoi(value); err == nil {
+						channel.AutoBan = &v
+					}
+				}
+			case "标签":
+				if value != "" {
+					channel.Tag = &value
+				}
+			case "额外设置":
+				if value != "" {
+					channel.Setting = &value
+				}
+			case "参数覆盖":
+				if value != "" {
+					channel.ParamOverride = &value
+				}
+			}
+		}
+
+		// 设置默认值
+		if channel.CreatedTime == 0 {
+			channel.CreatedTime = common.GetTimestamp()
+		}
+
+		// 如果没有设置状态,默认为启用
+		if channel.Status == 0 {
+			channel.Status = 1
+		}
+
+		channels = append(channels, channel)
+	}
+
+	// 批量插入渠道
+	err = model.BatchInsertChannels(channels)
+	if err != nil {
+		common.ApiError(c, fmt.Errorf("failed to insert channels: %w", err))
+		return
+	}
+
+	// 初始化渠道缓存
+	model.InitChannelCache()
+
+	// 返回成功响应
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": fmt.Sprintf("成功导入 %d 个渠道", len(channels)),
+	})
+}

+ 19 - 4
controller/log.go

@@ -146,15 +146,30 @@ func GetLogsSelfStat(c *gin.Context) {
 }
 
 func DeleteHistoryLogs(c *gin.Context) {
-	targetTimestamp, _ := strconv.ParseInt(c.Query("target_timestamp"), 10, 64)
-	if targetTimestamp == 0 {
+	// 获取开始和结束时间戳参数
+	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	
+	// 兼容旧的target_timestamp参数
+	if startTimestamp == 0 && endTimestamp == 0 {
+		targetTimestamp, _ := strconv.ParseInt(c.Query("target_timestamp"), 10, 64)
+		if targetTimestamp != 0 {
+			// 如果只提供了target_timestamp,则将其作为结束时间,开始时间为0(删除该时间之前的所有日志)
+			endTimestamp = targetTimestamp
+		}
+	}
+	
+	// 验证参数
+	if startTimestamp == 0 && endTimestamp == 0 {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
-			"message": "target timestamp is required",
+			"message": "start timestamp or end timestamp is required",
 		})
 		return
 	}
-	count, err := model.DeleteOldLog(c.Request.Context(), targetTimestamp, 100)
+	
+	// 调用模型层函数删除日志
+	count, err := model.DeleteLogsByTimeRange(c.Request.Context(), startTimestamp, endTimestamp, 100)
 	if err != nil {
 		common.ApiError(c, err)
 		return

+ 12 - 6
go.mod

@@ -32,10 +32,11 @@ require (
 	github.com/stripe/stripe-go/v81 v81.4.0
 	github.com/thanhpk/randstr v1.0.6
 	github.com/tiktoken-go/tokenizer v0.6.2
-	golang.org/x/crypto v0.35.0
-	golang.org/x/image v0.23.0
-	golang.org/x/net v0.35.0
-	golang.org/x/sync v0.11.0
+	github.com/xuri/excelize/v2 v2.9.1
+	golang.org/x/crypto v0.38.0
+	golang.org/x/image v0.25.0
+	golang.org/x/net v0.40.0
+	golang.org/x/sync v0.14.0
 	gorm.io/driver/mysql v1.4.3
 	gorm.io/driver/postgres v1.5.2
 	gorm.io/gorm v1.25.2
@@ -82,15 +83,20 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.1 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.4 // indirect
+	github.com/tiendc/go-deepcopy v1.6.0 // indirect
 	github.com/tklauser/go-sysconf v0.3.12 // indirect
 	github.com/tklauser/numcpus v0.6.1 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
+	github.com/xuri/efp v0.0.1 // indirect
+	github.com/xuri/nfp v0.0.1 // indirect
 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
 	golang.org/x/arch v0.12.0 // indirect
 	golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.25.0 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.22.5 // indirect

+ 27 - 13
go.sum

@@ -172,6 +172,11 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -193,12 +198,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stripe/stripe-go/v81 v81.4.0 h1:AuD9XzdAvl193qUCSaLocf8H+nRopOouXhxqJUzCLbw=
 github.com/stripe/stripe-go/v81 v81.4.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
 github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
 github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
+github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
+github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
 github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
 github.com/tiktoken-go/tokenizer v0.6.2/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
 github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -213,6 +221,12 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
+github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
+github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
+github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
@@ -221,19 +235,19 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
 golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
 golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
-golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
-golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
+golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
+golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -245,14 +259,14 @@ golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

+ 1 - 1
main.go

@@ -39,7 +39,7 @@ func main() {
 		return
 	}
 
-	common.SysLog("New API " + common.Version + " started")
+	common.SysLog("MIX API " + common.Version + " started")
 	if os.Getenv("GIN_MODE") != "debug" {
 		gin.SetMode(gin.ReleaseMode)
 	}

+ 35 - 0
model/log.go

@@ -431,3 +431,38 @@ func DeleteOldLog(ctx context.Context, targetTimestamp int64, limit int) (int64,
 
 	return total, nil
 }
+
+// DeleteLogsByTimeRange 根据时间范围删除日志
+func DeleteLogsByTimeRange(ctx context.Context, startTimestamp int64, endTimestamp int64, limit int) (int64, error) {
+	var total int64 = 0
+	var result *gorm.DB
+
+	for {
+		if nil != ctx.Err() {
+			return total, ctx.Err()
+		}
+
+		// 构建查询条件
+		query := LOG_DB
+		if startTimestamp > 0 {
+			query = query.Where("created_at >= ?", startTimestamp)
+		}
+		if endTimestamp > 0 {
+			query = query.Where("created_at <= ?", endTimestamp)
+		}
+
+		// 执行删除操作
+		result = query.Limit(limit).Delete(&Log{})
+		if nil != result.Error {
+			return total, result.Error
+		}
+
+		total += result.RowsAffected
+
+		if result.RowsAffected < int64(limit) {
+			break
+		}
+	}
+
+	return total, nil
+}

+ 3 - 0
model/option.go

@@ -44,6 +44,7 @@ func InitOptionMap() {
 	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
 	common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
 	common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
+	common.OptionMap["LogUserInputEnabled"] = strconv.FormatBool(common.LogUserInputEnabled)
 	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
 	common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
 	common.OptionMap["DrawingEnabled"] = strconv.FormatBool(common.DrawingEnabled)
@@ -234,6 +235,8 @@ func updateOptionMap(key string, value string) (err error) {
 			common.AutomaticEnableChannelEnabled = boolValue
 		case "LogConsumeEnabled":
 			common.LogConsumeEnabled = boolValue
+		case "LogUserInputEnabled":
+			common.LogUserInputEnabled = boolValue
 		case "DisplayInCurrencyEnabled":
 			common.DisplayInCurrencyEnabled = boolValue
 		case "DisplayTokenStatEnabled":

+ 62 - 11
relay/relay-text.go

@@ -22,6 +22,7 @@ import (
 	"one-api/types"
 	"strings"
 	"time"
+	"unicode"
 
 	"github.com/bytedance/gopkg/util/gopool"
 	"github.com/shopspring/decimal"
@@ -30,28 +31,76 @@ import (
 )
 
 // 提取用户输入内容(从 messages 中提取用户发送的消息)
+// 修改为只提取最后一条用户消息,避免记录历史输入
 func extractUserInputFromMessages(messages []dto.Message) string {
 	if len(messages) == 0 {
 		return ""
 	}
 
-	var userInputs []string
-	for _, message := range messages {
+	// 从后往前查找,只提取最后一条用户消息
+	for i := len(messages) - 1; i >= 0; i-- {
+		message := messages[i]
 		if message.Role == "user" {
 			content := message.StringContent()
 			if content != "" {
-				userInputs = append(userInputs, content)
+				// 过滤代码类内容
+				if isCodeContent(content) {
+					continue
+				}
+				// 只保留汉字内容
+				filteredContent := filterChineseContent(content)
+				if filteredContent != "" {
+					return filteredContent
+				}
 			}
 		}
 	}
 
-	// 如果有多个用户消息,用换行分隔
-	if len(userInputs) > 0 {
-		return strings.Join(userInputs, "\n")
+	// 如果没有找到合适的用户消息,返回最后一条消息的内容(作为备选)
+	lastContent := messages[len(messages)-1].StringContent()
+	// 过滤代码类内容
+	if isCodeContent(lastContent) {
+		return ""
+	}
+	// 只保留汉字内容
+	return filterChineseContent(lastContent)
+}
+
+// 检查是否为代码类内容
+func isCodeContent(content string) bool {
+	// 定义代码类关键词
+	codeKeywords := []string{
+		"VSCode Open Tabs",
+		"Current Time",
+		"Current Cost",
+		"Current Mode",
+		"REMINDERS",
+		"VSCode Visible Files",
+	}
+
+	// 检查是否包含代码类关键词
+	for _, keyword := range codeKeywords {
+		if strings.Contains(content, keyword) {
+			return true
+		}
 	}
 
-	// 如果没有用户消息,返回第一个消息的内容(作为备选)
-	return messages[0].StringContent()
+	return false
+}
+
+// 过滤并只保留汉字内容
+func filterChineseContent(content string) string {
+	// 使用正则表达式匹配汉字
+	var chineseContent strings.Builder
+	for _, r := range content {
+		// 检查字符是否为汉字
+		if unicode.Is(unicode.Scripts["Han"], r) {
+			chineseContent.WriteRune(r)
+		}
+	}
+
+	// 返回过滤后的汉字内容
+	return chineseContent.String()
 }
 
 func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) {
@@ -584,9 +633,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 
 	// 提取用户输入内容
 	var userInput string
-	if textRequestInterface, exists := ctx.Get("text_request"); exists {
-		if textRequest, ok := textRequestInterface.(*dto.GeneralOpenAIRequest); ok && textRequest != nil {
-			userInput = extractUserInputFromMessages(textRequest.Messages)
+	if common.LogUserInputEnabled {
+		if textRequestInterface, exists := ctx.Get("text_request"); exists {
+			if textRequest, ok := textRequestInterface.(*dto.GeneralOpenAIRequest); ok && textRequest != nil {
+				userInput = extractUserInputFromMessages(textRequest.Messages)
+			}
 		}
 	}
 

+ 2 - 0
router/api-router.go

@@ -120,6 +120,8 @@ func SetApiRouter(router *gin.Engine) {
 			channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
 			channelRoute.GET("/tag/models", controller.GetTagModels)
 			channelRoute.POST("/copy/:id", controller.CopyChannel)
+			channelRoute.GET("/export", controller.ExportChannels)
+			channelRoute.POST("/import", controller.ImportChannels)
 		}
 		tokenRoute := apiRouter.Group("/token")
 		tokenRoute.Use(middleware.UserAuth())