proxy.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package agentserver
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "github.com/gofiber/fiber/v2"
  12. "go.uber.org/zap"
  13. )
  14. // 需要解析的 JSON 响应最大读取字节数(事件列表最多 5000 条)
  15. const maxInspectBytes = 64 << 20
  16. // 透传给上游的请求头(Authorization 会被替换为 API Key,Cookie 等不转发)
  17. var forwardRequestHeaders = []string{"Content-Type", "Accept", "Last-Event-ID"}
  18. // 透传给浏览器的响应头
  19. var forwardResponseHeaders = []string{
  20. "Content-Type", "Content-Disposition", "Cache-Control",
  21. "X-Content-Type-Options", "Content-Security-Policy",
  22. }
  23. // Proxy /agent/v1/* → AgentServer /v1/*(technical-design §3.5)
  24. type Proxy struct {
  25. Client *Client
  26. Guard *Guard
  27. Sink ResourceSink
  28. // OnMessage 发送消息成功后回调(刷新会话 lastActive)
  29. OnMessage func(userId, sessionId string)
  30. // OnTitle 经代理改了标题后回调(同步归属表)
  31. OnTitle func(userId, sessionId, title string)
  32. }
  33. // WriteError 以 AgentServer 的错误格式返回,SDK 可以直接解析
  34. func WriteError(c *fiber.Ctx, status int, code, message string) error {
  35. return c.Status(status).JSON(fiber.Map{"error": fiber.Map{"code": code, "message": message}})
  36. }
  37. // Handle 处理一次代理请求;rest 为 /agent/v1/ 之后的路径
  38. func (p *Proxy) Handle(c *fiber.Ctx, userId, rest string) error {
  39. method := c.Method()
  40. path := "/" + strings.Trim(rest, "/")
  41. rawQuery := string(c.Request().URI().QueryString())
  42. query, _ := url.ParseQuery(rawQuery)
  43. route, ok := MatchRoute(method, path, query)
  44. if !ok {
  45. return WriteError(c, fiber.StatusForbidden, "forbidden", "不支持的接口")
  46. }
  47. sessionId, owned, err := p.Guard.Check(c.UserContext(), userId, route)
  48. if err != nil {
  49. zap.L().Warn("[Agent] 归属校验失败", zap.String("path", path), zap.Error(err))
  50. return WriteError(c, fiber.StatusBadGateway, "upstream_error", "AI 服务暂不可用")
  51. }
  52. if !owned {
  53. return WriteError(c, fiber.StatusNotFound, "not_found", "会话不存在或无权访问")
  54. }
  55. // 请求体:PATCH 只保留 title、model
  56. var body []byte
  57. var patchedTitle *string
  58. if route.Action == ActionSessionPatch {
  59. body, patchedTitle, err = filterSessionPatch(c.Body())
  60. if err != nil {
  61. return WriteError(c, fiber.StatusBadRequest, "bad_request", "请求体不是有效的 JSON")
  62. }
  63. } else if len(c.Body()) > 0 {
  64. body = append([]byte(nil), c.Body()...)
  65. }
  66. header := http.Header{}
  67. for _, h := range forwardRequestHeaders {
  68. if v := c.Get(h); v != "" {
  69. header.Set(h, v)
  70. }
  71. }
  72. if route.Action == ActionSessionPatch {
  73. header.Set("Content-Type", "application/json")
  74. }
  75. // 上游请求的生命周期:流式/透传响应在处理函数返回后才读取完,由读取方在结束时取消
  76. ctx, cancel := context.WithCancel(context.Background())
  77. var reqBody io.Reader
  78. if body != nil {
  79. reqBody = bytes.NewReader(body)
  80. }
  81. resp, err := p.Client.Forward(ctx, method, "/v1"+path, rawQuery, header, reqBody)
  82. if err != nil {
  83. cancel()
  84. zap.L().Warn("[Agent] 转发失败", zap.String("path", path), zap.Error(err))
  85. return WriteError(c, fiber.StatusBadGateway, "upstream_error", "AI 服务暂不可用")
  86. }
  87. for _, h := range forwardResponseHeaders {
  88. if v := resp.Header.Get(h); v != "" {
  89. c.Set(h, v)
  90. }
  91. }
  92. c.Status(resp.StatusCode)
  93. switch {
  94. case route.Action == ActionStream && resp.StatusCode == http.StatusOK:
  95. p.stream(c, resp, cancel, sessionId)
  96. return nil
  97. case needsInspect(route.Action) && resp.StatusCode < 300:
  98. defer cancel()
  99. defer resp.Body.Close()
  100. data, err := io.ReadAll(io.LimitReader(resp.Body, maxInspectBytes))
  101. if err != nil {
  102. return WriteError(c, fiber.StatusBadGateway, "upstream_error", "读取 AI 服务响应失败")
  103. }
  104. if rs := ResourcesFromResponse(route.Action, sessionId, data); len(rs) > 0 && p.Sink != nil {
  105. p.Sink.Record(rs)
  106. }
  107. if route.Action == ActionSendMessage && p.OnMessage != nil {
  108. p.OnMessage(userId, sessionId)
  109. }
  110. if route.Action == ActionSessionPatch && patchedTitle != nil && p.OnTitle != nil {
  111. p.OnTitle(userId, sessionId, *patchedTitle)
  112. }
  113. return c.Send(data)
  114. default:
  115. // 原样透传(文件下载等),读取结束时关闭上游连接
  116. c.Context().SetBodyStream(&cancelOnClose{ReadCloser: resp.Body, cancel: cancel}, int(resp.ContentLength))
  117. return nil
  118. }
  119. }
  120. func needsInspect(a Action) bool {
  121. switch a {
  122. case ActionEvents, ActionSessionDetail, ActionSessionPatch, ActionSendMessage,
  123. ActionUploadFiles, ActionListFiles, ActionInteractions:
  124. return true
  125. }
  126. return false
  127. }
  128. // stream SSE 透传:逐块写出并立即 Flush;旁路解析事件记录子资源;客户端断开时取消上游请求
  129. func (p *Proxy) stream(c *fiber.Ctx, resp *http.Response, cancel context.CancelFunc, sessionId string) {
  130. c.Set("Content-Type", "text/event-stream; charset=utf-8")
  131. c.Set("Cache-Control", "no-cache, no-transform")
  132. c.Set("Connection", "keep-alive")
  133. c.Set("X-Accel-Buffering", "no")
  134. scanner := NewSSEScanner(func(data []byte) {
  135. var rs []Resource
  136. for _, r := range ResourcesFromSSEData(data) {
  137. if r.SessionId == sessionId {
  138. rs = append(rs, r)
  139. }
  140. }
  141. if len(rs) > 0 && p.Sink != nil {
  142. p.Sink.Record(rs)
  143. }
  144. })
  145. c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
  146. defer cancel()
  147. defer resp.Body.Close()
  148. buf := make([]byte, 32<<10)
  149. for {
  150. n, err := resp.Body.Read(buf)
  151. if n > 0 {
  152. chunk := buf[:n]
  153. scanner.Write(chunk)
  154. if _, werr := w.Write(chunk); werr != nil {
  155. return
  156. }
  157. if ferr := w.Flush(); ferr != nil {
  158. // 客户端已断开
  159. return
  160. }
  161. }
  162. if err != nil {
  163. return
  164. }
  165. }
  166. })
  167. }
  168. // filterSessionPatch 只保留 title、model 字段
  169. func filterSessionPatch(raw []byte) ([]byte, *string, error) {
  170. in := map[string]json.RawMessage{}
  171. if len(bytes.TrimSpace(raw)) > 0 {
  172. if err := json.Unmarshal(raw, &in); err != nil {
  173. return nil, nil, err
  174. }
  175. }
  176. out := map[string]json.RawMessage{}
  177. var title *string
  178. for _, k := range []string{"title", "model"} {
  179. if v, ok := in[k]; ok {
  180. out[k] = v
  181. }
  182. }
  183. if v, ok := out["title"]; ok {
  184. var t string
  185. if json.Unmarshal(v, &t) == nil {
  186. title = &t
  187. }
  188. }
  189. b, err := json.Marshal(out)
  190. return b, title, err
  191. }
  192. // cancelOnClose 透传响应体读取结束(fasthttp 调用 Close)时取消上游请求
  193. type cancelOnClose struct {
  194. io.ReadCloser
  195. cancel context.CancelFunc
  196. }
  197. func (r *cancelOnClose) Close() error {
  198. err := r.ReadCloser.Close()
  199. r.cancel()
  200. return err
  201. }