content_moderation_handler.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. package admin
  2. import (
  3. "strconv"
  4. "strings"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/gin-gonic/gin"
  10. )
  11. type ContentModerationHandler struct {
  12. service *service.ContentModerationService
  13. }
  14. func NewContentModerationHandler(svc *service.ContentModerationService) *ContentModerationHandler {
  15. return &ContentModerationHandler{service: svc}
  16. }
  17. type contentModerationConfigRequest struct {
  18. Enabled *bool `json:"enabled"`
  19. Mode *string `json:"mode"`
  20. BaseURL *string `json:"base_url"`
  21. Model *string `json:"model"`
  22. // 审计请求使用的代理服务器:null 不修改;0 清除(直连);>0 指定代理。
  23. ProxyID *int64 `json:"proxy_id"`
  24. APIKey *string `json:"api_key"`
  25. APIKeys *[]string `json:"api_keys"`
  26. APIKeysMode string `json:"api_keys_mode"`
  27. DeleteAPIKeyHashes *[]string `json:"delete_api_key_hashes"`
  28. ClearAPIKey bool `json:"clear_api_key"`
  29. TimeoutMS *int `json:"timeout_ms"`
  30. SampleRate *int `json:"sample_rate"`
  31. AllGroups *bool `json:"all_groups"`
  32. GroupIDs *[]int64 `json:"group_ids"`
  33. RecordNonHits *bool `json:"record_non_hits"`
  34. Thresholds *map[string]float64 `json:"thresholds"`
  35. WorkerCount *int `json:"worker_count"`
  36. QueueSize *int `json:"queue_size"`
  37. BlockStatus *int `json:"block_status"`
  38. BlockMessage *string `json:"block_message"`
  39. EmailOnHit *bool `json:"email_on_hit"`
  40. AutoBanEnabled *bool `json:"auto_ban_enabled"`
  41. BanThreshold *int `json:"ban_threshold"`
  42. ViolationWindowHours *int `json:"violation_window_hours"`
  43. // cyber_policy 命中是否排除出自动封号计数;前端 RiskControlView 已发送该字段,
  44. // service.UpdateContentModerationConfigInput 已支持,此前 handler 层缺透传导致开关静默失效。
  45. CyberPolicyExcludeFromBanCount *bool `json:"cyber_policy_exclude_from_ban_count"`
  46. RetryCount *int `json:"retry_count"`
  47. HitRetentionDays *int `json:"hit_retention_days"`
  48. NonHitRetentionDays *int `json:"non_hit_retention_days"`
  49. PreHashCheckEnabled *bool `json:"pre_hash_check_enabled"`
  50. BlockedKeywords *[]string `json:"blocked_keywords"`
  51. KeywordBlockingMode *string `json:"keyword_blocking_mode"`
  52. ModelFilter *service.ContentModerationModelFilter `json:"model_filter"`
  53. }
  54. type contentModerationAPIKeyTestRequest struct {
  55. APIKeys []string `json:"api_keys"`
  56. BaseURL string `json:"base_url"`
  57. Model string `json:"model"`
  58. TimeoutMS int `json:"timeout_ms"`
  59. ProxyID *int64 `json:"proxy_id"`
  60. Prompt string `json:"prompt"`
  61. Images []string `json:"images"`
  62. }
  63. type contentModerationHashRequest struct {
  64. InputHash string `json:"input_hash"`
  65. }
  66. func (h *ContentModerationHandler) GetConfig(c *gin.Context) {
  67. cfg, err := h.service.GetConfig(c.Request.Context())
  68. if err != nil {
  69. response.ErrorFrom(c, err)
  70. return
  71. }
  72. response.Success(c, cfg)
  73. }
  74. func (h *ContentModerationHandler) UpdateConfig(c *gin.Context) {
  75. var req contentModerationConfigRequest
  76. if err := c.ShouldBindJSON(&req); err != nil {
  77. response.BadRequest(c, "Invalid request: "+err.Error())
  78. return
  79. }
  80. cfg, err := h.service.UpdateConfig(c.Request.Context(), service.UpdateContentModerationConfigInput{
  81. Enabled: req.Enabled,
  82. Mode: req.Mode,
  83. BaseURL: req.BaseURL,
  84. Model: req.Model,
  85. ProxyID: req.ProxyID,
  86. APIKey: req.APIKey,
  87. APIKeys: req.APIKeys,
  88. APIKeysMode: req.APIKeysMode,
  89. DeleteAPIKeyHashes: req.DeleteAPIKeyHashes,
  90. ClearAPIKey: req.ClearAPIKey,
  91. TimeoutMS: req.TimeoutMS,
  92. SampleRate: req.SampleRate,
  93. AllGroups: req.AllGroups,
  94. GroupIDs: req.GroupIDs,
  95. RecordNonHits: req.RecordNonHits,
  96. Thresholds: req.Thresholds,
  97. WorkerCount: req.WorkerCount,
  98. QueueSize: req.QueueSize,
  99. BlockStatus: req.BlockStatus,
  100. BlockMessage: req.BlockMessage,
  101. EmailOnHit: req.EmailOnHit,
  102. AutoBanEnabled: req.AutoBanEnabled,
  103. BanThreshold: req.BanThreshold,
  104. ViolationWindowHours: req.ViolationWindowHours,
  105. CyberPolicyExcludeFromBanCount: req.CyberPolicyExcludeFromBanCount,
  106. RetryCount: req.RetryCount,
  107. HitRetentionDays: req.HitRetentionDays,
  108. NonHitRetentionDays: req.NonHitRetentionDays,
  109. PreHashCheckEnabled: req.PreHashCheckEnabled,
  110. BlockedKeywords: req.BlockedKeywords,
  111. KeywordBlockingMode: req.KeywordBlockingMode,
  112. ModelFilter: req.ModelFilter,
  113. })
  114. if err != nil {
  115. response.ErrorFrom(c, err)
  116. return
  117. }
  118. response.Success(c, cfg)
  119. }
  120. func (h *ContentModerationHandler) TestAPIKeys(c *gin.Context) {
  121. var req contentModerationAPIKeyTestRequest
  122. if err := c.ShouldBindJSON(&req); err != nil {
  123. response.BadRequest(c, "Invalid request: "+err.Error())
  124. return
  125. }
  126. result, err := h.service.TestAPIKeys(c.Request.Context(), service.TestContentModerationAPIKeysInput{
  127. APIKeys: req.APIKeys,
  128. BaseURL: req.BaseURL,
  129. Model: req.Model,
  130. TimeoutMS: req.TimeoutMS,
  131. ProxyID: req.ProxyID,
  132. Prompt: req.Prompt,
  133. Images: req.Images,
  134. })
  135. if err != nil {
  136. response.ErrorFrom(c, err)
  137. return
  138. }
  139. response.Success(c, result)
  140. }
  141. func (h *ContentModerationHandler) GetStatus(c *gin.Context) {
  142. status, err := h.service.GetStatus(c.Request.Context())
  143. if err != nil {
  144. response.ErrorFrom(c, err)
  145. return
  146. }
  147. response.Success(c, status)
  148. }
  149. func (h *ContentModerationHandler) ListLogs(c *gin.Context) {
  150. page, pageSize := response.ParsePagination(c)
  151. filter := service.ContentModerationLogFilter{
  152. Pagination: pagination.PaginationParams{
  153. Page: page,
  154. PageSize: pageSize,
  155. SortOrder: pagination.SortOrderDesc,
  156. },
  157. Result: c.Query("result"),
  158. Endpoint: c.Query("endpoint"),
  159. Search: c.Query("search"),
  160. }
  161. if raw := strings.TrimSpace(c.Query("group_id")); raw != "" {
  162. groupID, err := strconv.ParseInt(raw, 10, 64)
  163. if err != nil || groupID <= 0 {
  164. response.BadRequest(c, "Invalid group_id")
  165. return
  166. }
  167. filter.GroupID = &groupID
  168. }
  169. if raw := strings.TrimSpace(c.Query("from")); raw != "" {
  170. t, _, err := parseContentModerationDate(raw)
  171. if err != nil {
  172. response.BadRequest(c, "Invalid from")
  173. return
  174. }
  175. filter.From = &t
  176. }
  177. if raw := strings.TrimSpace(c.Query("to")); raw != "" {
  178. t, dateOnly, err := parseContentModerationDate(raw)
  179. if err != nil {
  180. response.BadRequest(c, "Invalid to")
  181. return
  182. }
  183. if dateOnly {
  184. t = t.Add(24*time.Hour - time.Nanosecond)
  185. }
  186. filter.To = &t
  187. }
  188. items, pageResult, err := h.service.ListLogs(c.Request.Context(), filter)
  189. if err != nil {
  190. response.ErrorFrom(c, err)
  191. return
  192. }
  193. response.Paginated(c, items, pageResult.Total, pageResult.Page, pageResult.PageSize)
  194. }
  195. func (h *ContentModerationHandler) UnbanUser(c *gin.Context) {
  196. userID, err := strconv.ParseInt(strings.TrimSpace(c.Param("user_id")), 10, 64)
  197. if err != nil || userID <= 0 {
  198. response.BadRequest(c, "Invalid user_id")
  199. return
  200. }
  201. result, err := h.service.UnbanUser(c.Request.Context(), userID)
  202. if err != nil {
  203. response.ErrorFrom(c, err)
  204. return
  205. }
  206. response.Success(c, result)
  207. }
  208. func (h *ContentModerationHandler) DeleteFlaggedHash(c *gin.Context) {
  209. var req contentModerationHashRequest
  210. if err := c.ShouldBindJSON(&req); err != nil {
  211. response.BadRequest(c, "Invalid request: "+err.Error())
  212. return
  213. }
  214. result, err := h.service.DeleteFlaggedInputHash(c.Request.Context(), req.InputHash)
  215. if err != nil {
  216. response.ErrorFrom(c, err)
  217. return
  218. }
  219. response.Success(c, result)
  220. }
  221. func (h *ContentModerationHandler) ClearFlaggedHashes(c *gin.Context) {
  222. result, err := h.service.ClearFlaggedInputHashes(c.Request.Context())
  223. if err != nil {
  224. response.ErrorFrom(c, err)
  225. return
  226. }
  227. response.Success(c, result)
  228. }
  229. func parseContentModerationDate(raw string) (time.Time, bool, error) {
  230. raw = strings.TrimSpace(raw)
  231. if raw == "" {
  232. return time.Time{}, false, nil
  233. }
  234. if t, err := time.Parse(time.RFC3339, raw); err == nil {
  235. return t, false, nil
  236. }
  237. t, err := time.Parse("2006-01-02", raw)
  238. return t, err == nil, err
  239. }