package agentserver import ( "net/url" "strings" ) // OwnerKind 代理请求的归属校验方式 type OwnerKind int const ( // OwnerNone 只读元数据,无需归属校验(/v1/profiles、/v1/providers) OwnerNone OwnerKind = iota // OwnerSession 路径中的会话 ID OwnerSession // OwnerQuerySession 查询参数 sessionId OwnerQuerySession // OwnerRun 路径中的 runId OwnerRun // OwnerInteraction 路径中的 interactionId OwnerInteraction // OwnerFile 路径中的 fileId OwnerFile ) // Action 需要特殊处理的接口 type Action int const ( ActionPassthrough Action = iota ActionStream // GET /sessions/:id/stream(SSE 透传 + 解析事件) ActionEvents // GET /sessions/:id/events(解析事件) ActionSessionDetail // GET /sessions/:id(记录 activeRunId、待处理交互) ActionSessionPatch // PATCH /sessions/:id(只保留 title、model) ActionSendMessage // POST /sessions/:id/messages(记录 runId、刷新 lastActive) ActionUploadFiles // POST /sessions/:id/files(记录 fileId) ActionListFiles // GET /sessions/:id/files(记录 fileId) ActionInteractions // GET /interactions?sessionId=(记录 interactionId) ) // Route 白名单匹配结果 type Route struct { Owner OwnerKind Id string // 用于归属校验的 ID(会话 / run / 交互 / 文件) Action Action } // MatchRoute 按白名单(technical-design §3.5)匹配代理请求。 // path 为 /v1 之后的部分(如 /sessions/ses_x/stream);不在白名单内返回 false。 func MatchRoute(method, path string, query url.Values) (Route, bool) { seg := splitPath(path) n := len(seg) switch { case method == "GET" && n == 1 && (seg[0] == "profiles" || seg[0] == "providers"): return Route{Owner: OwnerNone}, true case n >= 2 && seg[0] == "sessions" && validId(seg[1], PrefixSession): id := seg[1] if n == 2 { switch method { case "GET": return Route{Owner: OwnerSession, Id: id, Action: ActionSessionDetail}, true case "PATCH": return Route{Owner: OwnerSession, Id: id, Action: ActionSessionPatch}, true } return Route{}, false } if n != 3 { return Route{}, false } switch { case method == "GET" && seg[2] == "events": return Route{Owner: OwnerSession, Id: id, Action: ActionEvents}, true case method == "GET" && seg[2] == "stream": return Route{Owner: OwnerSession, Id: id, Action: ActionStream}, true case method == "GET" && seg[2] == "files": return Route{Owner: OwnerSession, Id: id, Action: ActionListFiles}, true case method == "POST" && seg[2] == "files": return Route{Owner: OwnerSession, Id: id, Action: ActionUploadFiles}, true case method == "POST" && seg[2] == "messages": return Route{Owner: OwnerSession, Id: id, Action: ActionSendMessage}, true } return Route{}, false case n >= 2 && seg[0] == "runs" && validId(seg[1], PrefixRun): if (method == "GET" && n == 2) || (method == "POST" && n == 3 && seg[2] == "interrupt") { return Route{Owner: OwnerRun, Id: seg[1]}, true } return Route{}, false case method == "GET" && n == 1 && seg[0] == "interactions": sid := query.Get("sessionId") if !validId(sid, PrefixSession) { return Route{}, false } return Route{Owner: OwnerQuerySession, Id: sid, Action: ActionInteractions}, true case method == "POST" && n == 2 && seg[0] == "interactions" && validId(seg[1], PrefixInteraction): return Route{Owner: OwnerInteraction, Id: seg[1]}, true case method == "GET" && n == 2 && seg[0] == "files" && validId(seg[1], PrefixFile): return Route{Owner: OwnerFile, Id: seg[1]}, true } return Route{}, false } func splitPath(p string) []string { p = strings.Trim(p, "/") if p == "" { return nil } return strings.Split(p, "/") } // validId 校验带前缀的 ID,只允许字母、数字、下划线和连字符,防止路径穿越 func validId(id, prefix string) bool { if !strings.HasPrefix(id, prefix) || len(id) <= len(prefix) || len(id) > 64 { return false } for _, r := range id { if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') { return false } } return true } // ResourceKindOf 按前缀判断子资源类型 func ResourceKindOf(id string) OwnerKind { switch { case strings.HasPrefix(id, PrefixRun): return OwnerRun case strings.HasPrefix(id, PrefixInteraction): return OwnerInteraction case strings.HasPrefix(id, PrefixFile): return OwnerFile } return OwnerNone }