package app import ( "context" "database/sql" "errors" "fmt" "net/http" "strings" "sub2api-cn-relay-manager/internal/store/sqlite" ) type CreateLogicalGroupRequest struct { LogicalGroupID string `json:"logical_group_id"` DisplayName string `json:"display_name"` Status string `json:"status"` Description string `json:"description,omitempty"` UsageScenario string `json:"usage_scenario,omitempty"` Recommendation string `json:"recommendation,omitempty"` NextStepHint string `json:"next_step_hint,omitempty"` VisibilityScope string `json:"visibility_scope,omitempty"` PackageTier string `json:"package_tier,omitempty"` PurchaseCTALabel string `json:"purchase_cta_label,omitempty"` PurchaseCTAURL string `json:"purchase_cta_url,omitempty"` RoutePolicy string `json:"route_policy,omitempty"` StickyMode string `json:"sticky_mode,omitempty"` ConversationTTLSeconds int `json:"conversation_ttl_seconds,omitempty"` UserModelTTLSeconds int `json:"user_model_ttl_seconds,omitempty"` FailoverThreshold int `json:"failover_threshold,omitempty"` CooldownSeconds int `json:"cooldown_seconds,omitempty"` } type UpdateLogicalGroupRequest struct { LogicalGroupID string `json:"-"` DisplayName string `json:"display_name"` Status string `json:"status"` Description string `json:"description,omitempty"` UsageScenario string `json:"usage_scenario,omitempty"` Recommendation string `json:"recommendation,omitempty"` NextStepHint string `json:"next_step_hint,omitempty"` VisibilityScope string `json:"visibility_scope,omitempty"` PackageTier string `json:"package_tier,omitempty"` PurchaseCTALabel string `json:"purchase_cta_label,omitempty"` PurchaseCTAURL string `json:"purchase_cta_url,omitempty"` RoutePolicy string `json:"route_policy,omitempty"` StickyMode string `json:"sticky_mode,omitempty"` ConversationTTLSeconds int `json:"conversation_ttl_seconds,omitempty"` UserModelTTLSeconds int `json:"user_model_ttl_seconds,omitempty"` FailoverThreshold int `json:"failover_threshold,omitempty"` CooldownSeconds int `json:"cooldown_seconds,omitempty"` } type LogicalGroupInfo struct { LogicalGroupID string `json:"logical_group_id"` DisplayName string `json:"display_name"` Status string `json:"status"` Description string `json:"description,omitempty"` UsageScenario string `json:"usage_scenario,omitempty"` Recommendation string `json:"recommendation,omitempty"` NextStepHint string `json:"next_step_hint,omitempty"` VisibilityScope string `json:"visibility_scope,omitempty"` PackageTier string `json:"package_tier,omitempty"` PurchaseCTALabel string `json:"purchase_cta_label,omitempty"` PurchaseCTAURL string `json:"purchase_cta_url,omitempty"` RoutePolicy string `json:"route_policy,omitempty"` StickyMode string `json:"sticky_mode,omitempty"` ConversationTTLSeconds int `json:"conversation_ttl_seconds,omitempty"` UserModelTTLSeconds int `json:"user_model_ttl_seconds,omitempty"` FailoverThreshold int `json:"failover_threshold,omitempty"` CooldownSeconds int `json:"cooldown_seconds,omitempty"` Models []LogicalGroupModelInfo `json:"models,omitempty"` Routes []LogicalGroupRouteInfo `json:"routes,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } type CreateLogicalGroupModelRequest struct { LogicalGroupID string `json:"-"` PublicModel string `json:"public_model"` Status string `json:"status,omitempty"` } type DeleteLogicalGroupModelRequest struct { LogicalGroupID string PublicModel string } type LogicalGroupModelInfo struct { PublicModel string `json:"public_model"` Status string `json:"status,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } type CreateLogicalGroupRouteRequest struct { LogicalGroupID string `json:"-"` RouteID string `json:"route_id"` Name string `json:"name"` Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight,omitempty"` ShadowGroupID string `json:"shadow_group_id"` ShadowHostID string `json:"shadow_host_id"` UpstreamBaseURLHint string `json:"upstream_base_url_hint,omitempty"` CooldownUntil string `json:"cooldown_until,omitempty"` } type UpdateLogicalGroupRouteRequest struct { LogicalGroupID string `json:"-"` RouteID string `json:"-"` Name string `json:"name"` Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight,omitempty"` ShadowGroupID string `json:"shadow_group_id"` ShadowHostID string `json:"shadow_host_id"` UpstreamBaseURLHint string `json:"upstream_base_url_hint,omitempty"` CooldownUntil string `json:"cooldown_until,omitempty"` } type DeleteLogicalGroupRouteRequest struct { LogicalGroupID string RouteID string } type LogicalGroupRouteInfo struct { RouteID string `json:"route_id"` LogicalGroupID string `json:"logical_group_id"` Name string `json:"name"` Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight,omitempty"` ShadowGroupID string `json:"shadow_group_id"` ShadowHostID string `json:"shadow_host_id"` UpstreamBaseURLHint string `json:"upstream_base_url_hint,omitempty"` CooldownUntil string `json:"cooldown_until,omitempty"` Models []LogicalGroupRouteModelInfo `json:"models,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } type CreateLogicalGroupRouteModelRequest struct { LogicalGroupID string `json:"-"` RouteID string `json:"-"` PublicModel string `json:"public_model"` ShadowModel string `json:"shadow_model,omitempty"` Status string `json:"status,omitempty"` } type ListLogicalGroupRouteModelsRequest struct { LogicalGroupID string RouteID string } type LogicalGroupRouteModelInfo struct { PublicModel string `json:"public_model"` ShadowModel string `json:"shadow_model,omitempty"` Status string `json:"status,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } func handleCreateLogicalGroup(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateLogicalGroupRequest) (LogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-logical-group action is not configured"}) return } var req CreateLogicalGroupRequest if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } group, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusCreated, map[string]any{"logical_group": group}) } func handleListLogicalGroups(w http.ResponseWriter, r *http.Request, fn func(context.Context) ([]LogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-logical-groups action is not configured"}) return } groups, err := fn(r.Context()) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"logical_groups": groups}) } func handleGetLogicalGroup(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) (LogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-logical-group action is not configured"}) return } groupID := strings.TrimSpace(r.PathValue("groupID")) group, err := fn(r.Context(), groupID) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"logical_group": group}) } func handleUpdateLogicalGroup(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateLogicalGroupRequest) (LogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "update-logical-group action is not configured"}) return } var req UpdateLogicalGroupRequest req.LogicalGroupID = strings.TrimSpace(r.PathValue("groupID")) if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } group, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"logical_group": group}) } func handleDeleteLogicalGroup(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) error) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "delete-logical-group action is not configured"}) return } if err := fn(r.Context(), strings.TrimSpace(r.PathValue("groupID"))); err != nil { writeHTTPError(w, classifyError(err)) return } w.WriteHeader(http.StatusNoContent) } func handleCreateLogicalGroupModel(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateLogicalGroupModelRequest) (LogicalGroupModelInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-logical-group-model action is not configured"}) return } var req CreateLogicalGroupModelRequest req.LogicalGroupID = strings.TrimSpace(r.PathValue("groupID")) if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } model, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusCreated, map[string]any{"logical_group_model": model}) } func handleListLogicalGroupModels(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) ([]LogicalGroupModelInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-logical-group-models action is not configured"}) return } models, err := fn(r.Context(), strings.TrimSpace(r.PathValue("groupID"))) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"models": models}) } func handleDeleteLogicalGroupModel(w http.ResponseWriter, r *http.Request, fn func(context.Context, DeleteLogicalGroupModelRequest) error) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "delete-logical-group-model action is not configured"}) return } if err := fn(r.Context(), DeleteLogicalGroupModelRequest{ LogicalGroupID: strings.TrimSpace(r.PathValue("groupID")), PublicModel: strings.TrimSpace(r.PathValue("model")), }); err != nil { writeHTTPError(w, classifyError(err)) return } w.WriteHeader(http.StatusNoContent) } func handleCreateLogicalGroupRoute(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-logical-group-route action is not configured"}) return } var req CreateLogicalGroupRouteRequest req.LogicalGroupID = strings.TrimSpace(r.PathValue("groupID")) if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } route, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusCreated, map[string]any{"route": route}) } func handleListLogicalGroupRoutes(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) ([]LogicalGroupRouteInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-logical-group-routes action is not configured"}) return } routes, err := fn(r.Context(), strings.TrimSpace(r.PathValue("groupID"))) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"routes": routes}) } func handleUpdateLogicalGroupRoute(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "update-logical-group-route action is not configured"}) return } var req UpdateLogicalGroupRouteRequest req.LogicalGroupID = strings.TrimSpace(r.PathValue("groupID")) req.RouteID = strings.TrimSpace(r.PathValue("routeID")) if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } route, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"route": route}) } func handleDeleteLogicalGroupRoute(w http.ResponseWriter, r *http.Request, fn func(context.Context, DeleteLogicalGroupRouteRequest) error) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "delete-logical-group-route action is not configured"}) return } if err := fn(r.Context(), DeleteLogicalGroupRouteRequest{ LogicalGroupID: strings.TrimSpace(r.PathValue("groupID")), RouteID: strings.TrimSpace(r.PathValue("routeID")), }); err != nil { writeHTTPError(w, classifyError(err)) return } w.WriteHeader(http.StatusNoContent) } func handleCreateLogicalGroupRouteModel(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateLogicalGroupRouteModelRequest) (LogicalGroupRouteModelInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-logical-group-route-model action is not configured"}) return } var req CreateLogicalGroupRouteModelRequest req.LogicalGroupID = strings.TrimSpace(r.PathValue("groupID")) req.RouteID = strings.TrimSpace(r.PathValue("routeID")) if err := decodeJSON(r, &req); err != nil { writeHTTPError(w, err) return } model, err := fn(r.Context(), req) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusCreated, map[string]any{"route_model": model}) } func handleListLogicalGroupRouteModels(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListLogicalGroupRouteModelsRequest) ([]LogicalGroupRouteModelInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-logical-group-route-models action is not configured"}) return } models, err := fn(r.Context(), ListLogicalGroupRouteModelsRequest{ LogicalGroupID: strings.TrimSpace(r.PathValue("groupID")), RouteID: strings.TrimSpace(r.PathValue("routeID")), }) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"route_models": models}) } func buildCreateLogicalGroupAction(sqliteDSN string) func(context.Context, CreateLogicalGroupRequest) (LogicalGroupInfo, error) { return func(ctx context.Context, req CreateLogicalGroupRequest) (LogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupInfo{}, err } defer store.Close() row := logicalGroupRequestToRow(req) if _, err := store.LogicalGroups().Create(ctx, row); err != nil { return LogicalGroupInfo{}, err } return loadLogicalGroupInfo(ctx, store, row.LogicalGroupID) } } func buildListLogicalGroupsAction(sqliteDSN string) func(context.Context) ([]LogicalGroupInfo, error) { return func(ctx context.Context) ([]LogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() rows, err := store.LogicalGroups().List(ctx) if err != nil { return nil, err } result := make([]LogicalGroupInfo, 0, len(rows)) for _, row := range rows { info, err := loadLogicalGroupInfo(ctx, store, row.LogicalGroupID) if err != nil { return nil, err } result = append(result, info) } return result, nil } } func buildGetLogicalGroupAction(sqliteDSN string) func(context.Context, string) (LogicalGroupInfo, error) { return func(ctx context.Context, logicalGroupID string) (LogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupInfo{}, err } defer store.Close() return loadLogicalGroupInfo(ctx, store, logicalGroupID) } } func buildUpdateLogicalGroupAction(sqliteDSN string) func(context.Context, UpdateLogicalGroupRequest) (LogicalGroupInfo, error) { return func(ctx context.Context, req UpdateLogicalGroupRequest) (LogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupInfo{}, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID); err != nil { return LogicalGroupInfo{}, err } if err := store.LogicalGroups().UpdateByLogicalGroupID(ctx, logicalGroupRequestToRow(CreateLogicalGroupRequest{ LogicalGroupID: req.LogicalGroupID, DisplayName: req.DisplayName, Status: req.Status, Description: req.Description, UsageScenario: req.UsageScenario, Recommendation: req.Recommendation, NextStepHint: req.NextStepHint, VisibilityScope: req.VisibilityScope, PackageTier: req.PackageTier, PurchaseCTALabel: req.PurchaseCTALabel, PurchaseCTAURL: req.PurchaseCTAURL, RoutePolicy: req.RoutePolicy, StickyMode: req.StickyMode, ConversationTTLSeconds: req.ConversationTTLSeconds, UserModelTTLSeconds: req.UserModelTTLSeconds, FailoverThreshold: req.FailoverThreshold, CooldownSeconds: req.CooldownSeconds, })); err != nil { return LogicalGroupInfo{}, err } return loadLogicalGroupInfo(ctx, store, req.LogicalGroupID) } } func buildDeleteLogicalGroupAction(sqliteDSN string) func(context.Context, string) error { return func(ctx context.Context, logicalGroupID string) error { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return err } defer store.Close() return store.LogicalGroups().DeleteByLogicalGroupID(ctx, strings.TrimSpace(logicalGroupID)) } } func buildCreateLogicalGroupModelAction(sqliteDSN string) func(context.Context, CreateLogicalGroupModelRequest) (LogicalGroupModelInfo, error) { return func(ctx context.Context, req CreateLogicalGroupModelRequest) (LogicalGroupModelInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupModelInfo{}, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID); err != nil { return LogicalGroupModelInfo{}, err } row := sqlite.LogicalGroupModel{ LogicalGroupID: strings.TrimSpace(req.LogicalGroupID), PublicModel: strings.TrimSpace(req.PublicModel), Status: strings.TrimSpace(req.Status), } if _, err := store.LogicalGroupModels().Create(ctx, row); err != nil { return LogicalGroupModelInfo{}, err } models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, row.LogicalGroupID) if err != nil { return LogicalGroupModelInfo{}, err } for _, model := range models { if model.PublicModel == row.PublicModel { return logicalGroupModelRowToInfo(model), nil } } return LogicalGroupModelInfo{}, fmt.Errorf("logical group model %q/%q not found", row.LogicalGroupID, row.PublicModel) } } func buildListLogicalGroupModelsAction(sqliteDSN string) func(context.Context, string) ([]LogicalGroupModelInfo, error) { return func(ctx context.Context, logicalGroupID string) ([]LogicalGroupModelInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, logicalGroupID); err != nil { return nil, err } rows, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, logicalGroupID) if err != nil { return nil, err } return logicalGroupModelRowsToInfo(rows), nil } } func buildDeleteLogicalGroupModelAction(sqliteDSN string) func(context.Context, DeleteLogicalGroupModelRequest) error { return func(ctx context.Context, req DeleteLogicalGroupModelRequest) error { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID); err != nil { return err } return store.LogicalGroupModels().DeleteByLogicalGroupIDAndModel(ctx, strings.TrimSpace(req.LogicalGroupID), strings.TrimSpace(req.PublicModel)) } } func buildCreateLogicalGroupRouteAction(sqliteDSN string) func(context.Context, CreateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error) { return func(ctx context.Context, req CreateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupRouteInfo{}, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID); err != nil { return LogicalGroupRouteInfo{}, err } row := logicalGroupRouteRequestToRow(req) if _, err := store.LogicalGroupRoutes().Create(ctx, row); err != nil { return LogicalGroupRouteInfo{}, err } return loadLogicalGroupRouteInfo(ctx, store, row.RouteID) } } func buildListLogicalGroupRoutesAction(sqliteDSN string) func(context.Context, string) ([]LogicalGroupRouteInfo, error) { return func(ctx context.Context, logicalGroupID string) ([]LogicalGroupRouteInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, logicalGroupID); err != nil { return nil, err } rows, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, logicalGroupID) if err != nil { return nil, err } return logicalGroupRouteRowsToInfo(ctx, store, rows) } } func buildUpdateLogicalGroupRouteAction(sqliteDSN string) func(context.Context, UpdateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error) { return func(ctx context.Context, req UpdateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupRouteInfo{}, err } defer store.Close() if _, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID); err != nil { return LogicalGroupRouteInfo{}, err } existing, err := getLogicalGroupRouteRow(ctx, store, req.RouteID) if err != nil { return LogicalGroupRouteInfo{}, err } if existing.LogicalGroupID != strings.TrimSpace(req.LogicalGroupID) { return LogicalGroupRouteInfo{}, fmt.Errorf("logical group route %q not found under logical group %q", req.RouteID, req.LogicalGroupID) } row := logicalGroupRouteRequestToRow(CreateLogicalGroupRouteRequest{ LogicalGroupID: req.LogicalGroupID, RouteID: req.RouteID, Name: req.Name, Status: req.Status, Priority: req.Priority, Weight: req.Weight, ShadowGroupID: req.ShadowGroupID, ShadowHostID: req.ShadowHostID, UpstreamBaseURLHint: req.UpstreamBaseURLHint, CooldownUntil: req.CooldownUntil, }) if err := store.LogicalGroupRoutes().UpdateByRouteID(ctx, row); err != nil { return LogicalGroupRouteInfo{}, err } return loadLogicalGroupRouteInfo(ctx, store, req.RouteID) } } func buildDeleteLogicalGroupRouteAction(sqliteDSN string) func(context.Context, DeleteLogicalGroupRouteRequest) error { return func(ctx context.Context, req DeleteLogicalGroupRouteRequest) error { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return err } defer store.Close() existing, err := getLogicalGroupRouteRow(ctx, store, req.RouteID) if err != nil { return err } if existing.LogicalGroupID != strings.TrimSpace(req.LogicalGroupID) { return fmt.Errorf("logical group route %q not found under logical group %q", req.RouteID, req.LogicalGroupID) } return store.LogicalGroupRoutes().DeleteByRouteID(ctx, strings.TrimSpace(req.RouteID)) } } func buildCreateLogicalGroupRouteModelAction(sqliteDSN string) func(context.Context, CreateLogicalGroupRouteModelRequest) (LogicalGroupRouteModelInfo, error) { return func(ctx context.Context, req CreateLogicalGroupRouteModelRequest) (LogicalGroupRouteModelInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return LogicalGroupRouteModelInfo{}, err } defer store.Close() route, err := getLogicalGroupRouteRow(ctx, store, req.RouteID) if err != nil { return LogicalGroupRouteModelInfo{}, err } if route.LogicalGroupID != strings.TrimSpace(req.LogicalGroupID) { return LogicalGroupRouteModelInfo{}, fmt.Errorf("logical group route %q not found under logical group %q", req.RouteID, req.LogicalGroupID) } row := sqlite.LogicalGroupRouteModel{ RouteID: strings.TrimSpace(req.RouteID), PublicModel: strings.TrimSpace(req.PublicModel), ShadowModel: strings.TrimSpace(req.ShadowModel), Status: strings.TrimSpace(req.Status), } if _, err := store.LogicalGroupRouteModels().Create(ctx, row); err != nil { return LogicalGroupRouteModelInfo{}, err } models, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, row.RouteID) if err != nil { return LogicalGroupRouteModelInfo{}, err } for _, model := range models { if model.PublicModel == row.PublicModel { return logicalGroupRouteModelRowToInfo(model), nil } } return LogicalGroupRouteModelInfo{}, fmt.Errorf("logical group route model %q/%q not found", row.RouteID, row.PublicModel) } } func buildListLogicalGroupRouteModelsAction(sqliteDSN string) func(context.Context, ListLogicalGroupRouteModelsRequest) ([]LogicalGroupRouteModelInfo, error) { return func(ctx context.Context, req ListLogicalGroupRouteModelsRequest) ([]LogicalGroupRouteModelInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() route, err := getLogicalGroupRouteRow(ctx, store, req.RouteID) if err != nil { return nil, err } if route.LogicalGroupID != strings.TrimSpace(req.LogicalGroupID) { return nil, fmt.Errorf("logical group route %q not found under logical group %q", req.RouteID, req.LogicalGroupID) } rows, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, req.RouteID) if err != nil { return nil, err } return logicalGroupRouteModelRowsToInfo(rows), nil } } func logicalGroupRequestToRow(req CreateLogicalGroupRequest) sqlite.LogicalGroup { return sqlite.LogicalGroup{ LogicalGroupID: strings.TrimSpace(req.LogicalGroupID), DisplayName: strings.TrimSpace(req.DisplayName), Status: strings.TrimSpace(req.Status), Description: strings.TrimSpace(req.Description), UsageScenario: strings.TrimSpace(req.UsageScenario), Recommendation: strings.TrimSpace(req.Recommendation), NextStepHint: strings.TrimSpace(req.NextStepHint), VisibilityScope: strings.TrimSpace(req.VisibilityScope), PackageTier: strings.TrimSpace(req.PackageTier), PurchaseCTALabel: strings.TrimSpace(req.PurchaseCTALabel), PurchaseCTAURL: strings.TrimSpace(req.PurchaseCTAURL), RoutePolicy: strings.TrimSpace(req.RoutePolicy), StickyMode: strings.TrimSpace(req.StickyMode), ConversationTTLSeconds: req.ConversationTTLSeconds, UserModelTTLSeconds: req.UserModelTTLSeconds, FailoverThreshold: req.FailoverThreshold, CooldownSeconds: req.CooldownSeconds, } } func logicalGroupRouteRequestToRow(req CreateLogicalGroupRouteRequest) sqlite.LogicalGroupRoute { return sqlite.LogicalGroupRoute{ RouteID: strings.TrimSpace(req.RouteID), LogicalGroupID: strings.TrimSpace(req.LogicalGroupID), Name: strings.TrimSpace(req.Name), Status: strings.TrimSpace(req.Status), Priority: req.Priority, Weight: req.Weight, ShadowGroupID: strings.TrimSpace(req.ShadowGroupID), ShadowHostID: strings.TrimSpace(req.ShadowHostID), UpstreamBaseURLHint: strings.TrimSpace(req.UpstreamBaseURLHint), CooldownUntil: strings.TrimSpace(req.CooldownUntil), } } func loadLogicalGroupInfo(ctx context.Context, store *sqlite.DB, logicalGroupID string) (LogicalGroupInfo, error) { group, err := getLogicalGroupRow(ctx, store, logicalGroupID) if err != nil { return LogicalGroupInfo{}, err } models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, group.LogicalGroupID) if err != nil { return LogicalGroupInfo{}, err } routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, group.LogicalGroupID) if err != nil { return LogicalGroupInfo{}, err } routeInfos, err := logicalGroupRouteRowsToInfo(ctx, store, routes) if err != nil { return LogicalGroupInfo{}, err } return logicalGroupRowToInfo(group, logicalGroupModelRowsToInfo(models), routeInfos), nil } func loadLogicalGroupRouteInfo(ctx context.Context, store *sqlite.DB, routeID string) (LogicalGroupRouteInfo, error) { route, err := getLogicalGroupRouteRow(ctx, store, routeID) if err != nil { return LogicalGroupRouteInfo{}, err } models, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, route.RouteID) if err != nil { return LogicalGroupRouteInfo{}, err } return logicalGroupRouteRowToInfo(route, logicalGroupRouteModelRowsToInfo(models)), nil } func logicalGroupRowToInfo(group sqlite.LogicalGroup, models []LogicalGroupModelInfo, routes []LogicalGroupRouteInfo) LogicalGroupInfo { return LogicalGroupInfo{ LogicalGroupID: group.LogicalGroupID, DisplayName: group.DisplayName, Status: group.Status, Description: group.Description, UsageScenario: group.UsageScenario, Recommendation: group.Recommendation, NextStepHint: group.NextStepHint, VisibilityScope: group.VisibilityScope, PackageTier: group.PackageTier, PurchaseCTALabel: group.PurchaseCTALabel, PurchaseCTAURL: group.PurchaseCTAURL, RoutePolicy: group.RoutePolicy, StickyMode: group.StickyMode, ConversationTTLSeconds: group.ConversationTTLSeconds, UserModelTTLSeconds: group.UserModelTTLSeconds, FailoverThreshold: group.FailoverThreshold, CooldownSeconds: group.CooldownSeconds, Models: models, Routes: routes, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt, } } func logicalGroupModelRowsToInfo(rows []sqlite.LogicalGroupModel) []LogicalGroupModelInfo { result := make([]LogicalGroupModelInfo, 0, len(rows)) for _, row := range rows { result = append(result, logicalGroupModelRowToInfo(row)) } return result } func logicalGroupModelRowToInfo(row sqlite.LogicalGroupModel) LogicalGroupModelInfo { return LogicalGroupModelInfo{ PublicModel: row.PublicModel, Status: row.Status, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func logicalGroupRouteRowsToInfo(ctx context.Context, store *sqlite.DB, rows []sqlite.LogicalGroupRoute) ([]LogicalGroupRouteInfo, error) { result := make([]LogicalGroupRouteInfo, 0, len(rows)) for _, row := range rows { models, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, row.RouteID) if err != nil { return nil, err } result = append(result, logicalGroupRouteRowToInfo(row, logicalGroupRouteModelRowsToInfo(models))) } return result, nil } func logicalGroupRouteRowToInfo(row sqlite.LogicalGroupRoute, models []LogicalGroupRouteModelInfo) LogicalGroupRouteInfo { return LogicalGroupRouteInfo{ RouteID: row.RouteID, LogicalGroupID: row.LogicalGroupID, Name: row.Name, Status: row.Status, Priority: row.Priority, Weight: row.Weight, ShadowGroupID: row.ShadowGroupID, ShadowHostID: row.ShadowHostID, UpstreamBaseURLHint: row.UpstreamBaseURLHint, CooldownUntil: row.CooldownUntil, Models: models, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func logicalGroupRouteModelRowsToInfo(rows []sqlite.LogicalGroupRouteModel) []LogicalGroupRouteModelInfo { result := make([]LogicalGroupRouteModelInfo, 0, len(rows)) for _, row := range rows { result = append(result, logicalGroupRouteModelRowToInfo(row)) } return result } func logicalGroupRouteModelRowToInfo(row sqlite.LogicalGroupRouteModel) LogicalGroupRouteModelInfo { return LogicalGroupRouteModelInfo{ PublicModel: row.PublicModel, ShadowModel: row.ShadowModel, Status: row.Status, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func getLogicalGroupRow(ctx context.Context, store *sqlite.DB, logicalGroupID string) (sqlite.LogicalGroup, error) { row, err := store.LogicalGroups().GetByLogicalGroupID(ctx, strings.TrimSpace(logicalGroupID)) if errors.Is(err, sql.ErrNoRows) { return sqlite.LogicalGroup{}, fmt.Errorf("logical group %q not found", strings.TrimSpace(logicalGroupID)) } return row, err } func getLogicalGroupRouteRow(ctx context.Context, store *sqlite.DB, routeID string) (sqlite.LogicalGroupRoute, error) { row, err := store.LogicalGroupRoutes().GetByRouteID(ctx, strings.TrimSpace(routeID)) if errors.Is(err, sql.ErrNoRows) { return sqlite.LogicalGroupRoute{}, fmt.Errorf("logical group route %q not found", strings.TrimSpace(routeID)) } return row, err }