prompt_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. package securityaudit
  2. import (
  3. "context"
  4. "errors"
  5. "strconv"
  6. "strings"
  7. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  10. "github.com/gin-gonic/gin"
  11. )
  12. type PromptAdminService interface {
  13. GetConfig() (PublicConfig, error)
  14. SaveConfig(context.Context, UpdateConfigRequest, int64) (PublicConfig, error)
  15. Probe(context.Context, ProbeRequest) ProbeResult
  16. Runtime(context.Context) RuntimeSnapshot
  17. ListEvents(context.Context, EventFilter, int, int) (*EventPage, error)
  18. GetEvent(context.Context, int64) (*Event, error)
  19. DeleteEvent(context.Context, int64) (*DeleteResult, error)
  20. DeleteEventsByIDs(context.Context, []int64) (*DeleteResult, error)
  21. PreviewDelete(context.Context, EventFilter, int64) (*DeletePreview, error)
  22. DeleteByFilter(context.Context, DeleteByFilterRequest, int64) (*DeleteResult, error)
  23. }
  24. type PromptAdminHandler struct{ service PromptAdminService }
  25. func NewPromptAdminHandler(service PromptAdminService) *PromptAdminHandler {
  26. return &PromptAdminHandler{service: service}
  27. }
  28. func (h *PromptAdminHandler) GetConfig(c *gin.Context) {
  29. config, err := h.service.GetConfig()
  30. if err != nil {
  31. response.ErrorFrom(c, err)
  32. return
  33. }
  34. response.Success(c, config)
  35. }
  36. func (h *PromptAdminHandler) UpdateConfig(c *gin.Context) {
  37. var request UpdateConfigRequest
  38. if err := c.ShouldBindJSON(&request); err != nil {
  39. setPromptAdminAudit(c, "failed", "prompt_audit_invalid_config_request", nil)
  40. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_config_request", "提示词审计配置请求无效"))
  41. return
  42. }
  43. config, err := h.service.SaveConfig(c.Request.Context(), request, adminID(c))
  44. if err != nil {
  45. setPromptAdminAudit(c, "failed", infraerrors.Reason(err), configAuditFields(request, nil))
  46. response.ErrorFrom(c, err)
  47. return
  48. }
  49. setPromptAdminAudit(c, "success", "", configAuditFields(request, &config))
  50. response.Success(c, config)
  51. }
  52. func (h *PromptAdminHandler) ProbeEndpoint(c *gin.Context) {
  53. var request ProbeRequest
  54. if err := c.ShouldBindJSON(&request); err != nil {
  55. setPromptAdminAudit(c, "failed", "prompt_audit_invalid_probe_request", nil)
  56. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_probe_request", "审计节点探测请求无效"))
  57. return
  58. }
  59. result := h.service.Probe(c.Request.Context(), request)
  60. status := "failed"
  61. if result.OK {
  62. status = "success"
  63. }
  64. setPromptAdminAudit(c, status, result.ErrorCode, map[string]any{
  65. "guard_endpoint_id": request.Endpoint.ID, "http_status": result.HTTPStatus,
  66. "latency_ms": result.LatencyMS, "token_applied": result.TokenApplied, "retryable": result.Retryable,
  67. })
  68. response.Success(c, result)
  69. }
  70. func (h *PromptAdminHandler) GetRuntime(c *gin.Context) {
  71. response.Success(c, h.service.Runtime(c.Request.Context()))
  72. }
  73. func (h *PromptAdminHandler) ListEvents(c *gin.Context) {
  74. page, err := positiveIntQuery(c, "page", 1, 0)
  75. if err != nil {
  76. response.ErrorFrom(c, err)
  77. return
  78. }
  79. pageSize, err := positiveIntQuery(c, "page_size", 20, 100)
  80. if err != nil {
  81. response.ErrorFrom(c, err)
  82. return
  83. }
  84. filter, err := eventFilterFromQuery(c)
  85. if err != nil {
  86. response.ErrorFrom(c, err)
  87. return
  88. }
  89. result, err := h.service.ListEvents(c.Request.Context(), filter, page, pageSize)
  90. if err != nil {
  91. response.ErrorFrom(c, err)
  92. return
  93. }
  94. response.Success(c, result)
  95. }
  96. func (h *PromptAdminHandler) GetEvent(c *gin.Context) {
  97. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  98. if err != nil || id <= 0 {
  99. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_event_id", "事件 ID 无效"))
  100. return
  101. }
  102. event, err := h.service.GetEvent(c.Request.Context(), id)
  103. if errors.Is(err, ErrEventNotFound) {
  104. response.ErrorFrom(c, infraerrors.NotFound("prompt_audit_event_not_found", "提示词审计事件不存在"))
  105. return
  106. }
  107. if err != nil {
  108. response.ErrorFrom(c, err)
  109. return
  110. }
  111. response.Success(c, event)
  112. }
  113. func (h *PromptAdminHandler) DeleteEvent(c *gin.Context) {
  114. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  115. if err != nil || id <= 0 {
  116. setPromptAdminAudit(c, "failed", "prompt_audit_invalid_event_id", nil)
  117. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_event_id", "事件 ID 无效"))
  118. return
  119. }
  120. result, err := h.service.DeleteEvent(c.Request.Context(), id)
  121. if err != nil {
  122. setPromptAdminAudit(c, "failed", infraerrors.Reason(err), map[string]any{"event_id": id})
  123. response.ErrorFrom(c, err)
  124. return
  125. }
  126. setPromptAdminAudit(c, "success", "", deleteAuditFields(result, map[string]any{"event_id": id}))
  127. LogWarn(EventEventDeleted, map[string]any{"user_id": adminID(c), "event_id": id, "status": "deleted"})
  128. response.Success(c, result)
  129. }
  130. type batchDeleteRequest struct {
  131. IDs []int64 `json:"ids" binding:"required"`
  132. }
  133. func (h *PromptAdminHandler) BatchDelete(c *gin.Context) {
  134. var request batchDeleteRequest
  135. if err := c.ShouldBindJSON(&request); err != nil || len(request.IDs) == 0 || len(request.IDs) > 500 {
  136. setPromptAdminAudit(c, "failed", "prompt_audit_invalid_delete_batch", nil)
  137. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_delete_batch", "批量删除必须包含 1-500 个事件 ID"))
  138. return
  139. }
  140. for _, id := range request.IDs {
  141. if id <= 0 {
  142. setPromptAdminAudit(c, "failed", "prompt_audit_invalid_event_id", map[string]any{"requested_count": len(request.IDs)})
  143. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_invalid_event_id", "事件 ID 无效"))
  144. return
  145. }
  146. }
  147. result, err := h.service.DeleteEventsByIDs(c.Request.Context(), request.IDs)
  148. if err != nil {
  149. setPromptAdminAudit(c, "failed", infraerrors.Reason(err), map[string]any{"requested_count": len(request.IDs)})
  150. response.ErrorFrom(c, err)
  151. return
  152. }
  153. setPromptAdminAudit(c, "success", "", deleteAuditFields(result, map[string]any{"requested_count": len(request.IDs)}))
  154. LogWarn(EventEventsDeleted, map[string]any{"user_id": adminID(c), "status": "deleted"})
  155. response.Success(c, result)
  156. }
  157. func (h *PromptAdminHandler) DeletePreview(c *gin.Context) {
  158. var filter EventFilter
  159. if err := c.ShouldBindJSON(&filter); err != nil {
  160. setPromptAdminAudit(c, "failed", "prompt_audit_delete_preview_invalid", nil)
  161. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_delete_preview_invalid", "删除预览筛选无效"))
  162. return
  163. }
  164. preview, err := h.service.PreviewDelete(c.Request.Context(), filter, adminID(c))
  165. if err != nil {
  166. setPromptAdminAudit(c, "failed", "prompt_audit_delete_preview_invalid", nil)
  167. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_delete_preview_invalid", "删除预览筛选无效"))
  168. return
  169. }
  170. setPromptAdminAudit(c, "success", "", map[string]any{
  171. "matched_count": preview.MatchedCount, "snapshot_max_id": preview.SnapshotMaxID, "filter_hash": preview.FilterHash,
  172. })
  173. response.Success(c, preview)
  174. }
  175. func (h *PromptAdminHandler) DeleteByFilter(c *gin.Context) {
  176. var request DeleteByFilterRequest
  177. if err := c.ShouldBindJSON(&request); err != nil {
  178. setPromptAdminAudit(c, "failed", "prompt_audit_delete_confirmation_invalid", nil)
  179. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_delete_confirmation_invalid", "删除确认无效或已过期"))
  180. return
  181. }
  182. result, err := h.service.DeleteByFilter(c.Request.Context(), request, adminID(c))
  183. if err != nil {
  184. setPromptAdminAudit(c, "failed", "prompt_audit_delete_confirmation_invalid", map[string]any{
  185. "snapshot_max_id": request.SnapshotMaxID, "filter_hash": request.FilterHash, "confirm": request.Confirm,
  186. })
  187. response.ErrorFrom(c, infraerrors.BadRequest("prompt_audit_delete_confirmation_invalid", "删除确认无效或已过期"))
  188. return
  189. }
  190. setPromptAdminAudit(c, "success", "", deleteAuditFields(result, map[string]any{
  191. "snapshot_max_id": request.SnapshotMaxID, "filter_hash": request.FilterHash, "confirm": request.Confirm,
  192. }))
  193. response.Success(c, result)
  194. }
  195. func setPromptAdminAudit(c *gin.Context, result, errorCode string, fields map[string]any) {
  196. details := make(map[string]any, len(fields)+2)
  197. details["result"] = result
  198. if strings.TrimSpace(errorCode) != "" {
  199. details["error_code"] = errorCode
  200. }
  201. for key, value := range fields {
  202. details[key] = value
  203. }
  204. middleware.SetAuditExtra(c, details)
  205. }
  206. func configAuditFields(request UpdateConfigRequest, saved *PublicConfig) map[string]any {
  207. version := request.ExpectedConfigVersion
  208. if saved != nil {
  209. version = saved.ConfigVersion
  210. }
  211. return map[string]any{
  212. "enabled": request.Enabled, "blocking_enabled": request.BlockingEnabled,
  213. "blocking_latest_turn_only": request.BlockingLatestTurnOnly,
  214. "config_version": version, "endpoint_count": len(request.Endpoints),
  215. "scanner_count": len(request.Scanners), "all_groups": request.AllGroups,
  216. "group_count": len(request.GroupIDs),
  217. }
  218. }
  219. func deleteAuditFields(result *DeleteResult, base map[string]any) map[string]any {
  220. fields := make(map[string]any, len(base)+2)
  221. for key, value := range base {
  222. fields[key] = value
  223. }
  224. if result != nil {
  225. fields["deleted_events"] = result.DeletedEvents
  226. fields["deleted_jobs"] = result.DeletedJobs
  227. }
  228. return fields
  229. }
  230. func adminID(c *gin.Context) int64 {
  231. subject, ok := middleware.GetAuthSubjectFromContext(c)
  232. if !ok {
  233. return 0
  234. }
  235. return subject.UserID
  236. }
  237. func eventFilterFromQuery(c *gin.Context) (EventFilter, error) {
  238. groupID, err := optionalPositiveInt64Query(c, "group_id")
  239. if err != nil {
  240. return EventFilter{}, err
  241. }
  242. userID, err := optionalPositiveInt64Query(c, "user_id")
  243. if err != nil {
  244. return EventFilter{}, err
  245. }
  246. apiKeyID, err := optionalPositiveInt64Query(c, "api_key_id")
  247. if err != nil {
  248. return EventFilter{}, err
  249. }
  250. filter := EventFilter{
  251. Decision: c.Query("decision"), RiskLevel: c.Query("risk_level"), Endpoint: c.Query("endpoint"),
  252. GroupID: groupID, UserID: userID, APIKeyID: apiKeyID, RequestID: c.Query("request_id"),
  253. PromptHash: c.Query("prompt_hash"), Keyword: c.Query("keyword"),
  254. }
  255. if value := strings.TrimSpace(c.Query("start_at")); value != "" {
  256. filter.StartAt = parseTimeQuery(value)
  257. if filter.StartAt == nil {
  258. return EventFilter{}, infraerrors.BadRequest("prompt_audit_invalid_time", "开始时间无效")
  259. }
  260. }
  261. if value := strings.TrimSpace(c.Query("end_at")); value != "" {
  262. filter.EndAt = parseTimeQuery(value)
  263. if filter.EndAt == nil {
  264. return EventFilter{}, infraerrors.BadRequest("prompt_audit_invalid_time", "结束时间无效")
  265. }
  266. }
  267. return filter, nil
  268. }
  269. func optionalPositiveInt64Query(c *gin.Context, key string) (*int64, error) {
  270. value := strings.TrimSpace(c.Query(key))
  271. if value == "" {
  272. return nil, nil
  273. }
  274. parsed, err := strconv.ParseInt(value, 10, 64)
  275. if err != nil || parsed <= 0 {
  276. return nil, infraerrors.BadRequest("prompt_audit_invalid_filter_id", "事件筛选 ID 无效")
  277. }
  278. return &parsed, nil
  279. }
  280. func positiveIntQuery(c *gin.Context, key string, defaultValue, maxValue int) (int, error) {
  281. value := strings.TrimSpace(c.Query(key))
  282. if value == "" {
  283. return defaultValue, nil
  284. }
  285. parsed, err := strconv.Atoi(value)
  286. if err != nil || parsed <= 0 || (maxValue > 0 && parsed > maxValue) {
  287. return 0, infraerrors.BadRequest("prompt_audit_invalid_pagination", "分页参数无效")
  288. }
  289. return parsed, nil
  290. }