routes.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package agentserver
  2. import (
  3. "net/url"
  4. "strings"
  5. )
  6. // OwnerKind 代理请求的归属校验方式
  7. type OwnerKind int
  8. const (
  9. // OwnerNone 只读元数据,无需归属校验(/v1/profiles、/v1/providers)
  10. OwnerNone OwnerKind = iota
  11. // OwnerSession 路径中的会话 ID
  12. OwnerSession
  13. // OwnerQuerySession 查询参数 sessionId
  14. OwnerQuerySession
  15. // OwnerRun 路径中的 runId
  16. OwnerRun
  17. // OwnerInteraction 路径中的 interactionId
  18. OwnerInteraction
  19. // OwnerFile 路径中的 fileId
  20. OwnerFile
  21. )
  22. // Action 需要特殊处理的接口
  23. type Action int
  24. const (
  25. ActionPassthrough Action = iota
  26. ActionStream // GET /sessions/:id/stream(SSE 透传 + 解析事件)
  27. ActionEvents // GET /sessions/:id/events(解析事件)
  28. ActionSessionDetail // GET /sessions/:id(记录 activeRunId、待处理交互)
  29. ActionSessionPatch // PATCH /sessions/:id(只保留 title、model)
  30. ActionSendMessage // POST /sessions/:id/messages(记录 runId、刷新 lastActive)
  31. ActionUploadFiles // POST /sessions/:id/files(记录 fileId)
  32. ActionListFiles // GET /sessions/:id/files(记录 fileId)
  33. ActionInteractions // GET /interactions?sessionId=(记录 interactionId)
  34. )
  35. // Route 白名单匹配结果
  36. type Route struct {
  37. Owner OwnerKind
  38. Id string // 用于归属校验的 ID(会话 / run / 交互 / 文件)
  39. Action Action
  40. }
  41. // MatchRoute 按白名单(technical-design §3.5)匹配代理请求。
  42. // path 为 /v1 之后的部分(如 /sessions/ses_x/stream);不在白名单内返回 false。
  43. func MatchRoute(method, path string, query url.Values) (Route, bool) {
  44. seg := splitPath(path)
  45. n := len(seg)
  46. switch {
  47. case method == "GET" && n == 1 && (seg[0] == "profiles" || seg[0] == "providers"):
  48. return Route{Owner: OwnerNone}, true
  49. case n >= 2 && seg[0] == "sessions" && validId(seg[1], PrefixSession):
  50. id := seg[1]
  51. if n == 2 {
  52. switch method {
  53. case "GET":
  54. return Route{Owner: OwnerSession, Id: id, Action: ActionSessionDetail}, true
  55. case "PATCH":
  56. return Route{Owner: OwnerSession, Id: id, Action: ActionSessionPatch}, true
  57. }
  58. return Route{}, false
  59. }
  60. if n != 3 {
  61. return Route{}, false
  62. }
  63. switch {
  64. case method == "GET" && seg[2] == "events":
  65. return Route{Owner: OwnerSession, Id: id, Action: ActionEvents}, true
  66. case method == "GET" && seg[2] == "stream":
  67. return Route{Owner: OwnerSession, Id: id, Action: ActionStream}, true
  68. case method == "GET" && seg[2] == "files":
  69. return Route{Owner: OwnerSession, Id: id, Action: ActionListFiles}, true
  70. case method == "POST" && seg[2] == "files":
  71. return Route{Owner: OwnerSession, Id: id, Action: ActionUploadFiles}, true
  72. case method == "POST" && seg[2] == "messages":
  73. return Route{Owner: OwnerSession, Id: id, Action: ActionSendMessage}, true
  74. }
  75. return Route{}, false
  76. case n >= 2 && seg[0] == "runs" && validId(seg[1], PrefixRun):
  77. if (method == "GET" && n == 2) || (method == "POST" && n == 3 && seg[2] == "interrupt") {
  78. return Route{Owner: OwnerRun, Id: seg[1]}, true
  79. }
  80. return Route{}, false
  81. case method == "GET" && n == 1 && seg[0] == "interactions":
  82. sid := query.Get("sessionId")
  83. if !validId(sid, PrefixSession) {
  84. return Route{}, false
  85. }
  86. return Route{Owner: OwnerQuerySession, Id: sid, Action: ActionInteractions}, true
  87. case method == "POST" && n == 2 && seg[0] == "interactions" && validId(seg[1], PrefixInteraction):
  88. return Route{Owner: OwnerInteraction, Id: seg[1]}, true
  89. case method == "GET" && n == 2 && seg[0] == "files" && validId(seg[1], PrefixFile):
  90. return Route{Owner: OwnerFile, Id: seg[1]}, true
  91. }
  92. return Route{}, false
  93. }
  94. func splitPath(p string) []string {
  95. p = strings.Trim(p, "/")
  96. if p == "" {
  97. return nil
  98. }
  99. return strings.Split(p, "/")
  100. }
  101. // validId 校验带前缀的 ID,只允许字母、数字、下划线和连字符,防止路径穿越
  102. func validId(id, prefix string) bool {
  103. if !strings.HasPrefix(id, prefix) || len(id) <= len(prefix) || len(id) > 64 {
  104. return false
  105. }
  106. for _, r := range id {
  107. if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
  108. return false
  109. }
  110. }
  111. return true
  112. }
  113. // ResourceKindOf 按前缀判断子资源类型
  114. func ResourceKindOf(id string) OwnerKind {
  115. switch {
  116. case strings.HasPrefix(id, PrefixRun):
  117. return OwnerRun
  118. case strings.HasPrefix(id, PrefixInteraction):
  119. return OwnerInteraction
  120. case strings.HasPrefix(id, PrefixFile):
  121. return OwnerFile
  122. }
  123. return OwnerNone
  124. }