From e547c82f246db2d811de39f73e07ba683ea05690 Mon Sep 17 00:00:00 2001 From: DCK-c Date: Mon, 2 Mar 2026 22:54:34 +0800 Subject: [PATCH 1/5] feat: whitelist of ai_summary and add trace_id --- apis/hole/apis.go | 20 ++++++++++++++++---- apis/hole/schemas.go | 1 + config/config.go | 1 + models/hole.go | 13 +++++-------- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apis/hole/apis.go b/apis/hole/apis.go index 7b1e7c23..68eedecc 100644 --- a/apis/hole/apis.go +++ b/apis/hole/apis.go @@ -14,6 +14,7 @@ import ( "treehole_next/utils/sensitive" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" "github.com/opentreehole/go-common" "github.com/rs/zerolog/log" "gorm.io/gorm" @@ -951,7 +952,13 @@ func DeleteHole(c *fiber.Ctx) error { return c.Status(204).JSON(nil) } func GenerateSummary(c *fiber.Ctx) error { - + uid, _ := common.GetUserID(c) + if config.Config.WhiteListUserIds != nil && !slices.Contains(config.Config.WhiteListUserIds, uid) { + return c.Status(404).JSON(fiber.Map{ + "code": 404, + "message": "Cannot GET " + c.Path(), + }) + } id, _ := c.ParamsInt("id") forceRefresh := c.QueryBool("force_refresh") var cachedData Summary @@ -1041,6 +1048,7 @@ func GenerateSummary(c *fiber.Ctx) error { if cachedData.Data.HoleID != id && cachedData.Data.HoleID != 0 { log.Error(). Int("hole_id", id). + Str("trace_id", cachedData.TraceID). Int("ai_server_return_id", cachedData.Data.HoleID). Msg("AISummary: hole id error") cachedData.Data.HoleID = id @@ -1099,9 +1107,10 @@ func GenerateSummary(c *fiber.Ctx) error { } requestBody := map[string]any{ - "floors": summaryFloors, - "content": content, - "hole_id": hole.ID, + "floors": summaryFloors, + "content": content, + "hole_id": hole.ID, + "trace_id": uuid.NewString(), } requestJSON, err := json.Marshal(requestBody) @@ -1128,6 +1137,7 @@ func GenerateSummary(c *fiber.Ctx) error { if resp.StatusCode != http.StatusOK { log.Error(). Str("url", config.Config.AISummaryURL+"/generate_summary"). + Str("trace_id", requestBody["trace_id"].(string)). Int("status", resp.StatusCode). Str("req_body_base64", func() string { if len(requestJSON) > config.Config.SummaryLogLimit { @@ -1151,6 +1161,7 @@ func GenerateSummary(c *fiber.Ctx) error { if err != nil { log.Error(). Str("url", config.Config.AISummaryURL+"/generate_summary"). + Str("trace_id", requestBody["trace_id"].(string)). Int("status", resp.StatusCode). Str("req_body_base64", func() string { if len(requestJSON) > config.Config.SummaryLogLimit { @@ -1184,6 +1195,7 @@ func GenerateSummary(c *fiber.Ctx) error { response.Message = errCode2Message[response.Code] default: log.Error().Str("url", config.Config.AISummaryURL+"/generate_summary"). + Str("trace_id", requestBody["trace_id"].(string)). Int("status", resp.StatusCode). Str("req_body_base64", func() string { if len(requestJSON) > config.Config.SummaryLogLimit { diff --git a/apis/hole/schemas.go b/apis/hole/schemas.go index c01dc513..33940bc8 100644 --- a/apis/hole/schemas.go +++ b/apis/hole/schemas.go @@ -133,6 +133,7 @@ func (body ModifyModel) DoNothing() bool { type Summary struct { Code int `json:"code"` + TraceID string `json:"trace_id"` Message string `json:"message"` Data struct { HoleID int `json:"hole_id"` diff --git a/config/config.go b/config/config.go index adbedee4..224cd07b 100644 --- a/config/config.go +++ b/config/config.go @@ -60,6 +60,7 @@ var Config struct { SummaryContentLimit int64 `env:"SUMMARY_CONTENT_LIMIT" envDefault:"500"` SummarySteps int `env:"SUMMARY_STEPS" envDefault:"5"` SummaryLogLimit int `env:"SUMMARY_LOG_LIMIT" envDefault:"1000"` + WhiteListUserIds []int `env:"WHITE_LIST_USER_IDS"` } var DynamicConfig struct { diff --git a/models/hole.go b/models/hole.go index 75d7db31..973424c3 100644 --- a/models/hole.go +++ b/models/hole.go @@ -2,7 +2,6 @@ package models import ( "fmt" - "strconv" "time" "golang.org/x/exp/maps" @@ -276,7 +275,8 @@ func (holes Holes) Preprocess(c *fiber.Ctx) error { hole.SetHoleFloor() floors = append(floors, hole.Floors...) // set ai_summary_available - hole.AISummaryAvailable = true + uid, _ := common.GetUserID(c) + hole.AISummaryAvailable = config.Config.WhiteListUserIds == nil || slices.Contains(config.Config.WhiteListUserIds, uid) for _, tag := range hole.Tags { if len(tag.Name) > 0 && tag.Name[0] == '*' { hole.AISummaryAvailable = false @@ -294,14 +294,11 @@ func (holes Holes) Preprocess(c *fiber.Ctx) error { return err } - var discard any - hole.AISummaryAvailable = hole.Reply > config.Config.SummaryFloorLimit || contentSum >= config.Config.SummaryContentLimit || utils.GetCache("AISummary"+strconv.Itoa(hole.ID), &discard) + // var discard any + hole.AISummaryAvailable = hole.Reply > config.Config.SummaryFloorLimit || contentSum >= config.Config.SummaryContentLimit // || utils.GetCache("AISummary"+strconv.Itoa(hole.ID), &discard) if hole.AISummaryAvailable { - err = query.Where("is_sensitive = ?", true).Count(&sensitiveCount).Error - if err != nil { - return err - } + query.Where("is_sensitive = ? AND is_actual_sensitive IS NULL", true).Count(&sensitiveCount) if sensitiveCount > 0 { hole.AISummaryAvailable = false } From 0e40ad6c99d039fcc70f1c5cd25ee678bc46bc28 Mon Sep 17 00:00:00 2001 From: DCK-c Date: Wed, 4 Mar 2026 16:05:51 +0800 Subject: [PATCH 2/5] feat: disable division cache --- apis/division/apis.go | 7 ++++--- apis/hole/apis.go | 5 +++++ apis/hole/schemas.go | 6 ++++++ models/division.go | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apis/division/apis.go b/apis/division/apis.go index b6715dcb..0848b874 100644 --- a/apis/division/apis.go +++ b/apis/division/apis.go @@ -67,9 +67,10 @@ func AddDivision(c *fiber.Ctx) error { // @Success 200 {array} models.Division func ListDivisions(c *fiber.Ctx) error { var divisions Divisions - if GetCache("divisions", &divisions) { - return c.JSON(divisions) - } + // Temporarily disable cache + //if GetCache("divisions", &divisions) { + // return c.JSON(divisions) + //} err := DB.Find(&divisions, "hidden = false").Error if err != nil { return err diff --git a/apis/hole/apis.go b/apis/hole/apis.go index 68eedecc..e7ceadeb 100644 --- a/apis/hole/apis.go +++ b/apis/hole/apis.go @@ -1214,5 +1214,10 @@ func GenerateSummary(c *fiber.Ctx) error { } func GetFeedback(c *fiber.Ctx) error { + var body SummaryFeedback + err := common.ValidateBody(c, &body) + if err != nil { + return err + } return c.Status(200).JSON(fiber.Map{}) } diff --git a/apis/hole/schemas.go b/apis/hole/schemas.go index 33940bc8..1762a2aa 100644 --- a/apis/hole/schemas.go +++ b/apis/hole/schemas.go @@ -180,3 +180,9 @@ type Response struct { Message string `json:"message"` Data struct{} `json:"data"` } + +type SummaryFeedback struct { + TraceID string `json:"trace_id"` + Type int `json:"type"` + Reason string `json:"reason"` +} diff --git a/models/division.go b/models/division.go index 15ed8c75..1aa5e82e 100644 --- a/models/division.go +++ b/models/division.go @@ -57,7 +57,7 @@ func (divisions Divisions) Preprocess(c *fiber.Ctx) error { return err } } - return utils.SetCache("divisions", divisions, 0) + return nil //utils.SetCache("divisions", divisions, 0) } func (division *Division) Preprocess(c *fiber.Ctx) error { From 9c76079f72f0f00e7dec06fa1f936e076f76a4d6 Mon Sep 17 00:00:00 2001 From: DCK-c Date: Wed, 4 Mar 2026 17:46:45 +0800 Subject: [PATCH 3/5] fix: char length err in mysql --- models/hole.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/models/hole.go b/models/hole.go index 973424c3..215d4541 100644 --- a/models/hole.go +++ b/models/hole.go @@ -289,7 +289,12 @@ func (holes Holes) Preprocess(c *fiber.Ctx) error { var contentSum int64 var sensitiveCount int64 - err := query.Select("COALESCE(SUM(LENGTH(content)), 0)").Scan(&contentSum).Error + contentSumQuery := "SUM(CHAR_LENGTH(content))" + if DB.Dialector.Name() == "sqlite" { + contentSumQuery = "SUM(LENGTH(content))" + } + + err := query.Select(fmt.Sprintf("COALESCE(%s, 0)", contentSumQuery)).Scan(&contentSum).Error if err != nil { return err } From 4c4a8eee0b6126bfb2b38b411546b88bfbc8a9ea Mon Sep 17 00:00:00 2001 From: DCK-c Date: Thu, 5 Mar 2026 23:47:52 +0800 Subject: [PATCH 4/5] feat: dynamically update step of ai summary --- apis/hole/apis.go | 12 ++++++------ config/config.go | 3 +++ models/hole.go | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apis/hole/apis.go b/apis/hole/apis.go index e7ceadeb..ab98695e 100644 --- a/apis/hole/apis.go +++ b/apis/hole/apis.go @@ -992,12 +992,12 @@ func GenerateSummary(c *fiber.Ctx) error { }) } if forceRefresh { - err := DeleteCache(fmt.Sprintf("AISummary_%d_v%d", id, hole.Reply-hole.Reply%config.Config.SummarySteps)) + err := DeleteCache(hole.SummaryCacheName()) if err != nil { log.Err(err).Msg("AISummary: delete cache err") } } - if GetCache(fmt.Sprintf("AISummary_%d_v%d", id, hole.Reply-hole.Reply%config.Config.SummarySteps), &cachedData) { + if GetCache(hole.SummaryCacheName(), &cachedData) { switch cachedData.Code { case 1000: @@ -1053,7 +1053,7 @@ func GenerateSummary(c *fiber.Ctx) error { Msg("AISummary: hole id error") cachedData.Data.HoleID = id } - err = SetCache(fmt.Sprintf("AISummary_%d_v%d", id, hole.Reply-hole.Reply%config.Config.SummarySteps), cachedData, 24*time.Hour) + err = SetCache(hole.SummaryCacheName(), cachedData, 24*time.Hour) if err != nil { log.Err(err).Msg("AISummary: set cache err") } @@ -1061,14 +1061,14 @@ func GenerateSummary(c *fiber.Ctx) error { return c.Status(200).JSON(cachedData) } else { - err := DeleteCache(fmt.Sprintf("AISummary_%d_v%d", id, hole.Reply-hole.Reply%config.Config.SummarySteps)) + err := DeleteCache(hole.SummaryCacheName()) if err != nil { log.Err(err).Msg("AISummary: delete cache err") } } default: - err := DeleteCache("AISummary" + strconv.Itoa(id)) + err := DeleteCache(hole.SummaryCacheName()) if err != nil { log.Err(err).Msg("AISummary: delete cache err") } @@ -1185,7 +1185,7 @@ func GenerateSummary(c *fiber.Ctx) error { case 1000, 1001: var cache Summary cache.Code = 1001 - err := SetCache(fmt.Sprintf("AISummary_%d_v%d", id, hole.Reply-hole.Reply%config.Config.SummarySteps), cache, 24*time.Hour) + err := SetCache(hole.SummaryCacheName(), cache, 24*time.Hour) if err != nil { log.Err(err).Msg("AISummary: set cache err") } diff --git a/config/config.go b/config/config.go index 224cd07b..b61654bd 100644 --- a/config/config.go +++ b/config/config.go @@ -59,6 +59,9 @@ var Config struct { SummaryFloorLimit int `env:"SUMMARY_FLOOR_LIMIT" envDefault:"15"` SummaryContentLimit int64 `env:"SUMMARY_CONTENT_LIMIT" envDefault:"500"` SummarySteps int `env:"SUMMARY_STEPS" envDefault:"5"` + SummaryReplyBoundary1 int `env:"SUMMARY_REPLY_BOUNDARY_1" envDefault:"50"` + SummaryReplyBoundary2 int `env:"SUMMARY_REPLY_BOUNDARY_1" envDefault:"200"` + SummaryReplyBoundary3 int `env:"SUMMARY_REPLY_BOUNDARY_1" envDefault:"1000"` SummaryLogLimit int `env:"SUMMARY_LOG_LIMIT" envDefault:"1000"` WhiteListUserIds []int `env:"WHITE_LIST_USER_IDS"` } diff --git a/models/hole.go b/models/hole.go index 215d4541..7c67a120 100644 --- a/models/hole.go +++ b/models/hole.go @@ -98,6 +98,20 @@ func (hole *Hole) CacheName() string { return fmt.Sprintf("hole_%d", hole.ID) } +func (hole *Hole) SummaryCacheName() string { + var step int + if hole.Reply <= config.Config.SummaryReplyBoundary1 { + step = config.Config.SummarySteps + } else if hole.Reply > config.Config.SummaryReplyBoundary1 && hole.Reply <= config.Config.SummaryReplyBoundary2 { + step = config.Config.SummarySteps * 2 + } else if hole.Reply > config.Config.SummaryReplyBoundary2 && hole.Reply <= config.Config.SummaryReplyBoundary3 { + step = config.Config.SummarySteps * 4 + } else { + step = config.Config.SummarySteps * 10 + } + return fmt.Sprintf("AISummary_%d_v%d", hole.ID, hole.Reply-hole.Reply%step) +} + type Holes []*Hole func IsHolesExist(tx *gorm.DB, holeID []int) bool { From 4d361e27d92f8496b80c713bf0f9bd9d56fc5095 Mon Sep 17 00:00:00 2001 From: DCK-c Date: Sat, 7 Mar 2026 22:29:39 +0800 Subject: [PATCH 5/5] feat: only post 50 floors for summary --- apis/hole/apis.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++- config/config.go | 1 + 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apis/hole/apis.go b/apis/hole/apis.go index ab98695e..b582f53e 100644 --- a/apis/hole/apis.go +++ b/apis/hole/apis.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "net/http" "slices" "strconv" @@ -1087,6 +1088,54 @@ func GenerateSummary(c *fiber.Ctx) error { if err != nil { return err } + + if len(floors) > config.Config.MaxSummaryFloors { + // 1. 按 UpdatedAt 升序排序 + slices.SortFunc(floors, func(a, b *Floor) int { + if a.UpdatedAt.Equal(b.UpdatedAt) { + return 0 + } + if a.UpdatedAt.Before(b.UpdatedAt) { + return -1 + } + return 1 + }) + + // 2. 提取头尾 + earliestFloors := config.Config.MaxSummaryFloors / 5 + newestFloors := config.Config.MaxSummaryFloors / 5 * 2 + earliest := floors[:earliestFloors] + newest := floors[len(floors)-newestFloors:] + + // 3. 提取中间并按 Like 降序筛选 + middle := make(Floors, len(floors)-earliestFloors-newestFloors) + copy(middle, floors[earliestFloors:len(floors)-newestFloors]) // copy 防止影响原切片顺序 + + rand.Shuffle(len(middle), func(i, j int) { + middle[i], middle[j] = middle[j], middle[i] + }) + + slices.SortFunc(middle, func(a, b *Floor) int { + return b.Like - a.Like + }) + + // 4.即使中间不足20条也能安全截取 (len(floors)>50 保证了 middle 长度至少为 21) + topLikes := middle[:config.Config.MaxSummaryFloors-earliestFloors-newestFloors] + + // 5. 合并 + newFloors := make(Floors, 0, config.Config.MaxSummaryFloors) + newFloors = append(newFloors, earliest...) + newFloors = append(newFloors, topLikes...) + newFloors = append(newFloors, newest...) + + floors = newFloors + + // 6. 按 ID 重新排序以恢复对话流上下文 + slices.SortFunc(floors, func(a, b *Floor) int { + return a.Ranking - b.Ranking + }) + } + content := "" if hole.HoleFloor.FirstFloor != nil { content = hole.HoleFloor.FirstFloor.Content @@ -1105,7 +1154,6 @@ func GenerateSummary(c *fiber.Ctx) error { FloorID: floor.FloorID, } } - requestBody := map[string]any{ "floors": summaryFloors, "content": content, diff --git a/config/config.go b/config/config.go index b61654bd..1de28637 100644 --- a/config/config.go +++ b/config/config.go @@ -64,6 +64,7 @@ var Config struct { SummaryReplyBoundary3 int `env:"SUMMARY_REPLY_BOUNDARY_1" envDefault:"1000"` SummaryLogLimit int `env:"SUMMARY_LOG_LIMIT" envDefault:"1000"` WhiteListUserIds []int `env:"WHITE_LIST_USER_IDS"` + MaxSummaryFloors int `env:"MAX_FLOORS_PER_HOLE" envDefault:"50"` } var DynamicConfig struct {