Files
new-api/pkg/jsplugin/routing.go
T

932 lines
29 KiB
Go

package jsplugin
import (
"fmt"
"maps"
"net/http"
"regexp"
"slices"
"sort"
"strings"
"time"
"github.com/QuantumNous/new-api/constant"
)
type RouteType string
const (
RouteTypeSubmit RouteType = "submit"
RouteTypeQuery RouteType = "query"
RouteTypeDynamic RouteType = "dynamic"
)
type Route struct {
Method string `json:"method"`
Path string `json:"path"`
Type RouteType `json:"type"`
Action string `json:"action,omitempty"`
Decode string `json:"decode,omitempty"`
Render string `json:"render,omitempty"`
TaskIDParam string `json:"taskIdParam,omitempty"`
// Models restricts this route to the listed models. The host matches the
// canonical top-level "model" body field before any JS hook runs; empty
// means unrestricted. Must be a subset of meta.models.
Models []string `json:"models,omitempty"`
}
// ProtocolClaim is one entry of meta.protocols. Models narrows the protocol's
// endpoint bindings to a subset of meta.models; empty binds every model.
// Supports names the request forms a mode-bearing protocol accepts; decode
// and normalize rewrite it into host-table order.
type ProtocolClaim struct {
Name string `json:"name"`
Models []string `json:"models,omitempty"`
Supports []string `json:"supports,omitempty"`
objectForm bool
}
// ProtocolMode is one client request form a host protocol operation accepts
// and the plugin hook that implements it.
type ProtocolMode struct {
Name string
Hook string
}
type BodyKind string
const (
BodyNone BodyKind = "none"
BodyJSON BodyKind = "json"
BodyForm BodyKind = "form"
BodyMultipart BodyKind = "multipart"
)
type HostProtocolOperation struct {
Name string
Methods []string
Path string
BodyKinds []BodyKind
ModelField string
RequiredProtocolMembers []string
Modes []ProtocolMode
RequiredDriverHooks []string
}
type HostProtocolDefinition struct {
Name string
Operations []HostProtocolOperation
}
var hostProtocols = []HostProtocolDefinition{
{Name: "openai_responses", Operations: []HostProtocolOperation{
{Name: "create", Methods: []string{http.MethodPost}, Path: "/v1/responses", BodyKinds: []BodyKind{BodyJSON}, ModelField: "model", RequiredProtocolMembers: []string{"decodeRequest"}, Modes: []ProtocolMode{{Name: "stream", Hook: "renderEvents"}, {Name: "sync", Hook: "renderFinal"}, {Name: "background", Hook: "renderFinal"}}},
{Name: "retrieve", Methods: []string{http.MethodGet}, Path: "/v1/responses/:response_id", BodyKinds: []BodyKind{BodyNone}},
}},
{Name: "openai_video", Operations: []HostProtocolOperation{
{Name: "create", Methods: []string{http.MethodPost}, Path: "/v1/videos", BodyKinds: []BodyKind{BodyJSON, BodyMultipart}, ModelField: "model", RequiredProtocolMembers: []string{"decodeRequest"}},
{Name: "retrieve", Methods: []string{http.MethodGet}, Path: "/v1/videos/:task_id", BodyKinds: []BodyKind{BodyNone}, RequiredProtocolMembers: []string{"render"}},
{Name: "content", Methods: []string{http.MethodGet, http.MethodHead}, Path: "/v1/videos/:task_id/content", BodyKinds: []BodyKind{BodyNone}, RequiredDriverHooks: []string{"listArtifacts", "buildContentRequest"}},
}},
}
func HostProtocol(name string) (HostProtocolDefinition, bool) {
for _, definition := range hostProtocols {
if definition.Name == name {
return definition, true
}
}
return HostProtocolDefinition{}, false
}
func HostProtocols() []HostProtocolDefinition {
definitions := make([]HostProtocolDefinition, len(hostProtocols))
for index, definition := range hostProtocols {
definitions[index] = definition
definitions[index].Operations = append([]HostProtocolOperation(nil), definition.Operations...)
for operationIndex := range definitions[index].Operations {
operation := &definitions[index].Operations[operationIndex]
operation.Methods = append([]string(nil), operation.Methods...)
operation.BodyKinds = append([]BodyKind(nil), operation.BodyKinds...)
operation.RequiredProtocolMembers = append([]string(nil), operation.RequiredProtocolMembers...)
operation.Modes = append([]ProtocolMode(nil), operation.Modes...)
operation.RequiredDriverHooks = append([]string(nil), operation.RequiredDriverHooks...)
}
}
return definitions
}
// DefinedModes returns each distinct mode on the protocol in host-table order.
func (d HostProtocolDefinition) DefinedModes() []ProtocolMode {
seen := make(map[string]struct{})
modes := make([]ProtocolMode, 0)
for _, operation := range d.Operations {
for _, mode := range operation.Modes {
if _, exists := seen[mode.Name]; exists {
continue
}
seen[mode.Name] = struct{}{}
modes = append(modes, mode)
}
}
return modes
}
func orderProtocolSupports(protocol string, supports []string) []string {
if len(supports) == 0 {
return supports
}
definition, ok := HostProtocol(protocol)
if !ok {
return supports
}
rank := make(map[string]int)
for index, mode := range definition.DefinedModes() {
rank[mode.Name] = index
}
ordered := append([]string(nil), supports...)
slices.SortStableFunc(ordered, func(left, right string) int {
leftRank, leftKnown := rank[left]
rightRank, rightKnown := rank[right]
switch {
case leftKnown && rightKnown:
if leftRank < rightRank {
return -1
}
if leftRank > rightRank {
return 1
}
return 0
case leftKnown:
return -1
case rightKnown:
return 1
default:
return 0
}
})
return ordered
}
func LookupHostProtocolOperation(method, path string) (string, HostProtocolOperation, bool) {
method = strings.ToUpper(strings.TrimSpace(method))
for _, definition := range hostProtocols {
for _, operation := range definition.Operations {
if operation.Path != path || operation.ModelField == "" {
continue
}
if slices.Contains(operation.Methods, method) {
return definition.Name, operation, true
}
}
}
return "", HostProtocolOperation{}, false
}
type RouteBinding struct {
Plugin *LoadedPlugin
Route Route
}
type ProtocolBinding struct {
Plugin *LoadedPlugin
Protocol string
Operation HostProtocolOperation
Model string
}
const (
ContextKeyPinnedPlugin = "task_plugin_pinned_plugin"
ContextKeyPinnedRoute = "task_plugin_pinned_route"
ContextKeyPinnedEndpoint = "task_plugin_pinned_endpoint"
ContextKeyRouteRequest = "task_plugin_route_request"
ContextKeyProtocolRequest = "task_plugin_protocol_request"
)
type PinnedPlugin struct {
Generation *RoutingGeneration
Plugin *LoadedPlugin
}
type PinnedRoute struct {
Generation *RoutingGeneration
Plugin *LoadedPlugin
Route Route
}
// PinnedEndpoint carries the exact generation and endpoint candidates selected
// before distribution. Plugin initially names the deterministic request parser;
// distribution may rebind it to another candidate from the same generation
// when multiple legacy providers expose the same model.
type PinnedEndpoint struct {
Generation *RoutingGeneration
Plugin *LoadedPlugin
Protocol string
Operation HostProtocolOperation
Model string
Candidates []ProtocolBinding
}
// RouteRequestContext is the canonical request view exposed to declarative
// routing hooks. RequestBody contains decoded JSON or multipart text fields;
// raw binary and multipart file bytes remain host-owned.
type RouteRequestContext struct {
Path string `json:"path"`
Method string `json:"method"`
Params map[string]string `json:"params"`
Query map[string][]string `json:"query"`
Body any `json:"body"`
Files []map[string]any `json:"-"`
RequestBody any `json:"-"`
}
func (r RouteRequestContext) JSValue() map[string]any {
params := make(map[string]string, len(r.Params))
for key, value := range r.Params {
params[key] = value
}
query := make(map[string][]string, len(r.Query))
for key, values := range r.Query {
query[key] = append([]string(nil), values...)
}
return map[string]any{
"path": r.Path,
"method": r.Method,
"params": params,
"query": query,
"body": clonePluginRequestValue(r.Body),
}
}
func clonePluginRequestValue(value any) any {
switch typed := value.(type) {
case map[string]any:
cloned := make(map[string]any, len(typed))
for key, item := range typed {
cloned[key] = clonePluginRequestValue(item)
}
return cloned
case []any:
cloned := make([]any, len(typed))
for index, item := range typed {
cloned[index] = clonePluginRequestValue(item)
}
return cloned
case []string:
return append([]string(nil), typed...)
case map[string][]string:
cloned := make(map[string][]string, len(typed))
for key, values := range typed {
cloned[key] = append([]string(nil), values...)
}
return cloned
case []map[string]any:
cloned := make([]map[string]any, len(typed))
for index, item := range typed {
cloned[index] = clonePluginRequestValue(item).(map[string]any)
}
return cloned
default:
return value
}
}
type ProtocolRequestContext struct {
RouteRequestContext
Protocol string `json:"protocol"`
Operation string `json:"operation"`
Model string `json:"model"`
Stream bool `json:"stream"`
}
func (p ProtocolRequestContext) JSValue() map[string]any {
value := p.RouteRequestContext.JSValue()
value["protocol"] = p.Protocol
value["operation"] = p.Operation
value["model"] = p.Model
value["stream"] = p.Stream
return value
}
// SupportsHostProtocol reports whether the current host release has a
// concrete wire/state machine for an otherwise valid manifest endpoint.
func SupportsHostProtocol(protocol string) bool { _, ok := HostProtocol(protocol); return ok }
// RoutingGeneration is an immutable, request-pinnable view of all effective
// plugins and their deterministic routing indexes.
type RoutingGeneration struct {
Number uint64
PublishedAt time.Time
byKey map[string]*LoadedPlugin
byModel map[string]*LoadedPlugin
byChannelType map[int]*LoadedPlugin
routeIndex map[string]RouteBinding
protocolIndex map[string][]ProtocolBinding
plugins []*LoadedPlugin
routes []RouteBinding
runtime http.Handler
retainCurrent map[string]struct{}
}
var (
routeMethodPattern = regexp.MustCompile(`^(GET|POST|PUT|PATCH|DELETE)$`)
pathNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
staticSegment = regexp.MustCompile(`^[A-Za-z0-9._~-]+$`)
memberNamePattern = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`)
)
var reservedRouteNamespaces = []string{
"/api",
"/assets",
"/setup",
"/v1/tasks",
"/console",
"/login",
"/forbidden",
"/sign-in",
"/sign-up",
"/forgot-password",
"/oauth",
"/otp",
"/register",
"/reset",
"/privacy-policy",
"/user-agreement",
"/about",
"/pricing",
"/rankings",
"/user",
"/401",
"/403",
"/404",
"/500",
"/503",
"/chat2link",
"/system-settings",
"/channels",
"/chat",
"/dashboard",
"/errors",
"/keys",
"/models",
"/playground",
"/profile",
"/redemption-codes",
"/subscriptions",
"/system-info",
"/task-plugins",
"/usage-logs",
"/users",
"/wallet",
}
func (g *RoutingGeneration) Get(key string) (*LoadedPlugin, bool) {
if g == nil {
return nil, false
}
plugin, ok := g.byKey[key]
return plugin, ok
}
func (g *RoutingGeneration) GetByChannelType(channelType int) (*LoadedPlugin, bool) {
if g == nil || channelType == 0 || channelType == constant.ChannelTypeTaskPlugin {
return nil, false
}
plugin, ok := g.byChannelType[channelType]
return plugin, ok
}
// GetByModel returns the deterministic effective plugin metadata used for a
// model-level host concern such as billing. When multiple providers expose the
// same model name, the first plugin in generation order owns that shared
// metadata view.
func (g *RoutingGeneration) GetByModel(model string) (*LoadedPlugin, bool) {
if g == nil || model == "" {
return nil, false
}
plugin, ok := g.byModel[model]
return plugin, ok
}
// LookupDeclaredRoute resolves a manifest path declaration. It does not match
// an incoming concrete URL; runtime matching is delegated to Gin.
func (g *RoutingGeneration) LookupDeclaredRoute(method, path string) (RouteBinding, bool) {
if g == nil {
return RouteBinding{}, false
}
normalizedMethod, err := normalizeRouteMethod(method)
if err != nil {
return RouteBinding{}, false
}
shape, err := routePathShape(path)
if err != nil {
return RouteBinding{}, false
}
binding, ok := g.routeIndex[normalizedMethod+" "+shape]
return binding, ok
}
func (g *RoutingGeneration) LookupEndpoint(method, path, model string) (ProtocolBinding, bool) {
if g == nil {
return ProtocolBinding{}, false
}
normalizedMethod, err := normalizeRouteMethod(method)
if err != nil {
return ProtocolBinding{}, false
}
bindings := g.protocolIndex[endpointIndexKey(normalizedMethod, path, model)]
if len(bindings) == 0 {
return ProtocolBinding{}, false
}
return bindings[0], true
}
// LookupEndpointCandidates returns every legacy provider implementation that
// can serve one shared model endpoint. Candidate order is deterministic and
// the first binding is the parser used before channel distribution.
func (g *RoutingGeneration) LookupEndpointCandidates(method, path, model string) []ProtocolBinding {
if g == nil {
return nil
}
normalizedMethod, err := normalizeRouteMethod(method)
if err != nil {
return nil
}
bindings := g.protocolIndex[endpointIndexKey(normalizedMethod, path, model)]
return append([]ProtocolBinding(nil), bindings...)
}
func (g *RoutingGeneration) Plugins() []*LoadedPlugin {
if g == nil {
return nil
}
return append([]*LoadedPlugin(nil), g.plugins...)
}
func (g *RoutingGeneration) Routes() []RouteBinding {
if g == nil {
return nil
}
return append([]RouteBinding(nil), g.routes...)
}
// RuntimeHandler is the inner router built for this exact generation. It is
// published in the same atomic pointer as the routing indexes.
func (g *RoutingGeneration) RuntimeHandler() http.Handler {
if g == nil {
return nil
}
return g.runtime
}
func (g *RoutingGeneration) RetainsIncumbent(key string) bool {
if g == nil {
return false
}
_, retained := g.retainCurrent[key]
return retained
}
// RebuildWithPlugins creates a generation from the supplied exact plugin
// pointers. It is used when a rejected hot update must retain the incumbent
// runtime object for that key.
func (g *RoutingGeneration) RebuildWithPlugins(plugins []*LoadedPlugin) (*RoutingGeneration, error) {
if g == nil {
return nil, fmt.Errorf("cannot rebuild a nil routing generation")
}
byKey := make(map[string]*LoadedPlugin, len(plugins))
for _, plugin := range plugins {
if plugin == nil {
return nil, fmt.Errorf("cannot rebuild routing generation with a nil plugin")
}
if _, exists := g.byKey[plugin.Meta.Key]; !exists {
return nil, fmt.Errorf("plugin %q is not present in routing generation %d", plugin.Meta.Key, g.Number)
}
if _, duplicate := byKey[plugin.Meta.Key]; duplicate {
return nil, fmt.Errorf("plugin %q appears more than once in routing generation rebuild", plugin.Meta.Key)
}
byKey[plugin.Meta.Key] = plugin
}
rebuilt, err := buildRoutingGeneration(byKey, nil, false, g.Number)
if err != nil {
return nil, err
}
rebuilt.PublishedAt = g.PublishedAt
rebuilt.retainCurrent = cloneStringSet(g.retainCurrent)
return rebuilt, nil
}
// WithRuntime returns a shallow immutable copy carrying the prepared inner
// handler. Callers use it before publication; published generations must not
// be mutated.
func (g *RoutingGeneration) WithRuntime(handler http.Handler) *RoutingGeneration {
if g == nil {
return nil
}
prepared := *g
prepared.runtime = handler
return &prepared
}
func cloneStringSet(source map[string]struct{}) map[string]struct{} {
if len(source) == 0 {
return nil
}
clone := make(map[string]struct{}, len(source))
for key := range source {
clone[key] = struct{}{}
}
return clone
}
func normalizeRouteMethod(method string) (string, error) {
method = strings.ToUpper(strings.TrimSpace(method))
if !routeMethodPattern.MatchString(method) {
return "", fmt.Errorf("plugin route method %q is not supported", method)
}
return method, nil
}
// NormalizeRoutePath validates the canonical path syntax used by plugin route
// declarations. A trailing slash is allowed and remains significant.
func NormalizeRoutePath(routePath string) (string, error) {
if routePath == "" || routePath[0] != '/' {
return "", fmt.Errorf("plugin route path must start with /")
}
if routePath == "/" {
return "", fmt.Errorf("plugin route path / is reserved")
}
if strings.ContainsAny(routePath, "?#%") {
return "", fmt.Errorf("plugin route path %q must not contain a query, fragment, or percent-encoding", routePath)
}
if strings.Contains(routePath, "//") {
return "", fmt.Errorf("plugin route path %q must not contain empty segments", routePath)
}
segments := strings.Split(strings.TrimPrefix(routePath, "/"), "/")
seenNames := make(map[string]struct{})
for index, segment := range segments {
if segment == "" && index == len(segments)-1 {
continue
}
if segment == "." || segment == ".." {
return "", fmt.Errorf("plugin route path %q must not contain dot segments", routePath)
}
if after, ok := strings.CutPrefix(segment, ":"); ok {
name := after
if !pathNamePattern.MatchString(name) {
return "", fmt.Errorf("plugin route path %q has invalid parameter %q", routePath, segment)
}
if _, exists := seenNames[name]; exists {
return "", fmt.Errorf("plugin route path %q repeats parameter %q", routePath, name)
}
seenNames[name] = struct{}{}
continue
}
if after, ok := strings.CutPrefix(segment, "*"); ok {
name := after
if index != len(segments)-1 || !pathNamePattern.MatchString(name) {
return "", fmt.Errorf("plugin route path %q has an invalid catch-all segment", routePath)
}
if _, exists := seenNames[name]; exists {
return "", fmt.Errorf("plugin route path %q repeats parameter %q", routePath, name)
}
seenNames[name] = struct{}{}
continue
}
if !staticSegment.MatchString(segment) {
return "", fmt.Errorf("plugin route path %q has invalid segment %q", routePath, segment)
}
}
return routePath, nil
}
func routePathShape(routePath string) (string, error) {
normalized, err := NormalizeRoutePath(routePath)
if err != nil {
return "", err
}
segments := strings.Split(strings.TrimPrefix(normalized, "/"), "/")
for index, segment := range segments {
if strings.HasPrefix(segment, ":") {
segments[index] = ":"
} else if strings.HasPrefix(segment, "*") {
segments[index] = "*"
}
}
return "/" + strings.Join(segments, "/"), nil
}
func endpointIndexKey(method, path, model string) string {
return method + "\x00" + path + "\x00" + model
}
func intersectingReservedNamespace(routePath string) (string, bool) {
for _, namespace := range reservedRouteNamespaces {
if routePatternIntersectsNamespace(routePath, namespace) {
return namespace, true
}
}
return "", false
}
func routePatternIntersectsNamespace(routePath, namespace string) bool {
routeSegments := strings.Split(strings.Trim(strings.TrimPrefix(routePath, "/"), "/"), "/")
namespaceSegments := strings.Split(strings.TrimPrefix(namespace, "/"), "/")
for index, namespaceSegment := range namespaceSegments {
if index >= len(routeSegments) {
return false
}
routeSegment := routeSegments[index]
if strings.HasPrefix(routeSegment, "*") {
return true
}
if !strings.HasPrefix(routeSegment, ":") && routeSegment != namespaceSegment {
return false
}
}
return true
}
func validateRoute(route *Route) error {
if route.Method != strings.ToUpper(strings.TrimSpace(route.Method)) {
return fmt.Errorf("plugin route method %q must use canonical uppercase spelling", route.Method)
}
method, err := normalizeRouteMethod(route.Method)
if err != nil {
return err
}
route.Method = method
route.Path, err = NormalizeRoutePath(route.Path)
if err != nil {
return err
}
if namespace, reserved := intersectingReservedNamespace(route.Path); reserved {
return fmt.Errorf("plugin route path %q intersects reserved namespace %s", route.Path, namespace)
}
switch route.Type {
case RouteTypeSubmit:
if route.Decode == "" || route.Render == "" || route.TaskIDParam != "" {
return fmt.Errorf("submit route %s %s must declare decode and render and must not declare taskIdParam", route.Method, route.Path)
}
case RouteTypeQuery:
if route.Decode != "" || strings.TrimSpace(route.Render) == "" {
return fmt.Errorf("query route %s %s must declare render and must not declare decode", route.Method, route.Path)
}
if route.Action != "" {
return fmt.Errorf("query route %s %s must not declare action", route.Method, route.Path)
}
if route.TaskIDParam == "" {
route.TaskIDParam = "task_id"
}
if !pathNamePattern.MatchString(route.TaskIDParam) {
return fmt.Errorf("query route %s %s has invalid taskIdParam %q", route.Method, route.Path, route.TaskIDParam)
}
if !pathHasParameter(route.Path, route.TaskIDParam) {
return fmt.Errorf("query route %s %s must contain :%s", route.Method, route.Path, route.TaskIDParam)
}
case RouteTypeDynamic:
if route.Decode == "" || route.Render == "" || route.TaskIDParam != "" {
return fmt.Errorf("dynamic route %s %s must declare decode and render and must not declare taskIdParam", route.Method, route.Path)
}
default:
return fmt.Errorf("plugin route %s %s has unsupported type %q", route.Method, route.Path, route.Type)
}
if route.Decode != "" && !memberNamePattern.MatchString(route.Decode) {
return fmt.Errorf("plugin route %s %s has invalid decode %q", route.Method, route.Path, route.Decode)
}
if route.Render != "" && !memberNamePattern.MatchString(route.Render) {
return fmt.Errorf("plugin route %s %s has invalid render %q", route.Method, route.Path, route.Render)
}
if strings.TrimSpace(route.Action) != route.Action {
return fmt.Errorf("plugin route %s %s action must not have surrounding whitespace", route.Method, route.Path)
}
if len(route.Models) > 0 {
if route.Type == RouteTypeQuery {
return fmt.Errorf("query route %s %s must not declare models", route.Method, route.Path)
}
if err := validateModelScope(route.Models, fmt.Sprintf("route %s %s", route.Method, route.Path)); err != nil {
return err
}
}
return nil
}
// validateModelScope enforces the shared rules for route.models and
// per-protocol models entries: non-empty canonical names, no duplicates.
func validateModelScope(models []string, subject string) error {
seen := make(map[string]struct{}, len(models))
for _, model := range models {
if strings.TrimSpace(model) == "" || strings.TrimSpace(model) != model {
return fmt.Errorf("plugin %s models must contain non-empty canonical names", subject)
}
if _, duplicate := seen[model]; duplicate {
return fmt.Errorf("plugin %s models must be unique", subject)
}
seen[model] = struct{}{}
}
return nil
}
func pathHasParameter(routePath, name string) bool {
return slices.Contains(strings.Split(routePath, "/"), ":"+name)
}
func ResolveRouteAction(route Route, resolvedAction string) string {
if strings.TrimSpace(resolvedAction) != "" {
return resolvedAction
}
return route.Action
}
func buildRoutingGeneration(factory, override map[string]*LoadedPlugin, overrideEnabled bool, number uint64) (*RoutingGeneration, error) {
effective := effectivePlugins(factory, override, overrideEnabled)
return buildRoutingGenerationFromPlugins(effective, number)
}
func buildRoutingGenerationAdmitting(
factory, override map[string]*LoadedPlugin,
overrideEnabled bool,
number uint64,
current *RoutingGeneration,
retainCurrent map[string]struct{},
) (*RoutingGeneration, map[string]string, error) {
candidates := effectivePlugins(factory, override, overrideEnabled)
accepted := make(map[string]*LoadedPlugin, len(candidates))
currentByKey := make(map[string]*LoadedPlugin)
if current != nil {
for _, plugin := range current.plugins {
currentByKey[plugin.Meta.Key] = plugin
}
}
generation, err := buildRoutingGenerationFromPlugins(accepted, number)
if err != nil {
return nil, nil, fmt.Errorf("initialize routing generation: %w", err)
}
unchangedKeys := make([]string, 0)
changedKeys := make([]string, 0)
newKeys := make([]string, 0)
for key, candidate := range candidates {
incumbent, exists := currentByKey[key]
switch {
case exists && candidate == incumbent:
unchangedKeys = append(unchangedKeys, key)
case exists:
changedKeys = append(changedKeys, key)
default:
newKeys = append(newKeys, key)
}
}
sort.Strings(unchangedKeys)
sort.Strings(changedKeys)
sort.Strings(newKeys)
routingErrors := make(map[string]string)
orderedKeys := append(unchangedKeys, changedKeys...)
orderedKeys = append(orderedKeys, newKeys...)
rejectedKeys := make([]string, 0)
for _, key := range orderedKeys {
candidate := candidates[key]
accepted[key] = candidate
trial, trialErr := buildRoutingGenerationFromPlugins(accepted, number)
if trialErr == nil {
generation = trial
continue
}
delete(accepted, key)
routingErrors[key] = fmt.Sprintf("plugin %s rejected from routing generation: %v", key, trialErr)
rejectedKeys = append(rejectedKeys, key)
}
for _, key := range rejectedKeys {
candidate := candidates[key]
incumbent, hasIncumbent := currentByKey[key]
_, mayRetain := retainCurrent[key]
if !hasIncumbent || !mayRetain || incumbent == candidate {
continue
}
accepted[key] = incumbent
fallback, fallbackErr := buildRoutingGenerationFromPlugins(accepted, number)
if fallbackErr == nil {
generation = fallback
continue
}
delete(accepted, key)
}
generation.retainCurrent = cloneStringSet(retainCurrent)
return generation, routingErrors, nil
}
func effectivePlugins(factory, override map[string]*LoadedPlugin, overrideEnabled bool) map[string]*LoadedPlugin {
effective := make(map[string]*LoadedPlugin, len(factory)+len(override))
maps.Copy(effective, factory)
if overrideEnabled {
maps.Copy(effective, override)
}
return effective
}
func buildRoutingGenerationFromPlugins(effective map[string]*LoadedPlugin, number uint64) (*RoutingGeneration, error) {
keys := make([]string, 0, len(effective))
for key := range effective {
keys = append(keys, key)
}
sort.Strings(keys)
generation := &RoutingGeneration{
Number: number,
PublishedAt: time.Now(),
byKey: make(map[string]*LoadedPlugin, len(effective)),
byModel: make(map[string]*LoadedPlugin),
byChannelType: make(map[int]*LoadedPlugin),
routeIndex: make(map[string]RouteBinding),
protocolIndex: make(map[string][]ProtocolBinding),
plugins: make([]*LoadedPlugin, 0, len(effective)),
}
for _, key := range keys {
plugin := effective[key]
generation.byKey[key] = plugin
generation.plugins = append(generation.plugins, plugin)
for _, model := range plugin.Meta.Models {
if _, exists := generation.byModel[model]; !exists {
generation.byModel[model] = plugin
}
}
for _, channelType := range plugin.Meta.ChannelTypes {
if channelType == 0 || channelType == constant.ChannelTypeTaskPlugin {
continue
}
if other, exists := generation.byChannelType[channelType]; exists {
return nil, fmt.Errorf("plugin %s channelType %d conflicts with plugin %s", plugin.Meta.Key, channelType, other.Meta.Key)
}
generation.byChannelType[channelType] = plugin
}
for _, route := range plugin.Meta.Routes {
shape, err := routePathShape(route.Path)
if err != nil {
return nil, err
}
indexKey := route.Method + " " + shape
if other, exists := generation.routeIndex[indexKey]; exists {
return nil, fmt.Errorf("plugin %s route %s %s conflicts with plugin %s route %s", plugin.Meta.Key, route.Method, route.Path, other.Plugin.Meta.Key, other.Route.Path)
}
binding := RouteBinding{Plugin: plugin, Route: route}
generation.routeIndex[indexKey] = binding
generation.routes = append(generation.routes, binding)
}
for _, claim := range plugin.Meta.Protocols {
definition, _ := HostProtocol(claim.Name)
boundModels := plugin.Meta.Models
if len(claim.Models) > 0 {
boundModels = claim.Models
}
for _, operation := range definition.Operations {
if operation.ModelField == "" {
continue
}
for _, method := range operation.Methods {
for _, model := range boundModels {
indexKey := endpointIndexKey(method, operation.Path, model)
bindings := generation.protocolIndex[indexKey]
if len(bindings) > 0 {
other := bindings[0]
legacyProviders := len(plugin.Meta.ChannelTypes) > 0 && len(other.Plugin.Meta.ChannelTypes) > 0
if !legacyProviders || claim.Name != other.Protocol {
return nil, fmt.Errorf("plugin %s protocol %s %s model %q conflicts with plugin %s", plugin.Meta.Key, method, operation.Path, model, other.Plugin.Meta.Key)
}
}
generation.protocolIndex[indexKey] = append(bindings, ProtocolBinding{Plugin: plugin, Protocol: claim.Name, Operation: operation, Model: model})
}
}
}
}
}
return generation, nil
}
// PreflightRoutingConflict reports whether admitting candidate into the
// current generation would collide on a channel type, native route, or
// protocol-model binding. A same-key entry is replaced first so re-uploading
// a plugin (or overriding a factory built-in) does not self-conflict.
func PreflightRoutingConflict(current *RoutingGeneration, candidate *LoadedPlugin) error {
if candidate == nil {
return fmt.Errorf("cannot preflight a nil plugin")
}
effective := make(map[string]*LoadedPlugin)
number := uint64(0)
if current != nil {
for _, plugin := range current.Plugins() {
effective[plugin.Meta.Key] = plugin
}
number = current.Number
}
effective[candidate.Meta.Key] = candidate
_, err := buildRoutingGenerationFromPlugins(effective, number)
return err
}