2026-05-22 15:39:43 +08:00
package app
import (
"context"
"net/http"
"strconv"
"strings"
"sub2api-cn-relay-manager/internal/batch"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
type BatchImportEntryRequest struct {
BaseURL string ` json:"base_url" `
APIKey string ` json:"api_key" `
RequestedModels [ ] string ` json:"requested_models" `
}
type CreateBatchImportRunRequest struct {
HostID string ` json:"host_id,omitempty" `
Mode string ` json:"mode" `
AccessMode string ` json:"access_mode" `
ConfirmWaitTimeoutSec int ` json:"confirm_wait_timeout_sec,omitempty" `
SubscriptionUsers [ ] string ` json:"subscription_users,omitempty" `
SubscriptionDays int ` json:"subscription_days,omitempty" `
ProbeAPIKey string ` json:"probe_api_key,omitempty" `
Entries [ ] BatchImportEntryRequest ` json:"entries" `
}
type BatchImportRunCreateResponse struct {
RunID string ` json:"run_id" `
State string ` json:"state" `
ResultPage string ` json:"result_page" `
TotalItems int ` json:"total_items" `
ActiveItems int ` json:"active_items" `
DegradedItems int ` json:"degraded_items" `
BrokenItems int ` json:"broken_items" `
WarningItems int ` json:"warning_items" `
}
type ListBatchImportRunsRequest struct {
State string
AccessMode string
Query string
2026-05-23 09:18:02 +08:00
Cursor string
2026-05-22 15:39:43 +08:00
Limit int
}
2026-05-23 09:18:02 +08:00
type ListBatchImportRunsResponse struct {
Runs [ ] batch . RunSummaryProjection ` json:"runs" `
NextCursor * string ` json:"next_cursor" `
}
2026-05-22 15:39:43 +08:00
type ListBatchImportRunItemsRequest struct {
RunID string
CurrentStage string
ConfirmationStatus string
AccessStatus string
HasWarning * bool
ProviderID string
MatchedAccountState string
AccountResolution string
Query string
2026-05-23 09:18:02 +08:00
Cursor string
2026-05-22 15:39:43 +08:00
Limit int
}
2026-05-23 09:18:02 +08:00
type ListBatchImportRunItemsResponse struct {
Items [ ] batch . ItemSummaryProjection ` json:"items" `
NextCursor * string ` json:"next_cursor" `
}
2026-05-22 15:39:43 +08:00
type GetBatchImportRunItemRequest struct {
RunID string
ItemID string
}
func handleCreateBatchImportRun ( w http . ResponseWriter , r * http . Request , fn func ( context . Context , CreateBatchImportRunRequest ) ( BatchImportRunCreateResponse , error ) ) {
if fn == nil {
writeHTTPError ( w , & httpError { StatusCode : http . StatusInternalServerError , Code : "server_misconfigured" , Message : "create-batch-import-run action is not configured" } )
return
}
var req CreateBatchImportRunRequest
if err := decodeJSON ( r , & req ) ; err != nil {
writeHTTPError ( w , err )
return
}
if err := validateCreateBatchImportRunRequest ( req ) ; err != nil {
writeHTTPError ( w , err )
return
}
result , err := fn ( r . Context ( ) , req )
if err != nil {
writeHTTPError ( w , classifyError ( err ) )
return
}
writeJSON ( w , http . StatusOK , result )
}
2026-05-23 09:18:02 +08:00
func handleListBatchImportRuns ( w http . ResponseWriter , r * http . Request , fn func ( context . Context , ListBatchImportRunsRequest ) ( ListBatchImportRunsResponse , error ) ) {
2026-05-22 15:39:43 +08:00
if fn == nil {
writeHTTPError ( w , & httpError { StatusCode : http . StatusInternalServerError , Code : "server_misconfigured" , Message : "list-batch-import-runs action is not configured" } )
return
}
result , err := fn ( r . Context ( ) , ListBatchImportRunsRequest {
State : strings . TrimSpace ( r . URL . Query ( ) . Get ( "state" ) ) ,
AccessMode : strings . TrimSpace ( r . URL . Query ( ) . Get ( "access_mode" ) ) ,
Query : strings . TrimSpace ( r . URL . Query ( ) . Get ( "q" ) ) ,
2026-05-23 09:18:02 +08:00
Cursor : strings . TrimSpace ( r . URL . Query ( ) . Get ( "cursor" ) ) ,
2026-05-22 15:39:43 +08:00
Limit : parsePositiveInt ( r . URL . Query ( ) . Get ( "limit" ) ) ,
} )
if err != nil {
writeHTTPError ( w , classifyError ( err ) )
return
}
2026-05-23 09:18:02 +08:00
if result . Runs == nil {
result . Runs = [ ] batch . RunSummaryProjection { }
2026-05-22 15:39:43 +08:00
}
2026-05-23 09:18:02 +08:00
writeJSON ( w , http . StatusOK , result )
2026-05-22 15:39:43 +08:00
}
func buildCreateBatchImportRunAction ( sqliteDSN string ) func ( context . Context , CreateBatchImportRunRequest ) ( BatchImportRunCreateResponse , error ) {
return func ( ctx context . Context , req CreateBatchImportRunRequest ) ( BatchImportRunCreateResponse , error ) {
store , err := sqlite . Open ( ctx , sqliteDSN )
if err != nil {
return BatchImportRunCreateResponse { } , err
}
defer store . Close ( )
2026-05-22 16:12:52 +08:00
hostRow , client , err := resolveManagedHost ( ctx , store , req . HostID , "" , CreateHostAuth { } )
if err != nil {
2026-05-22 15:39:43 +08:00
return BatchImportRunCreateResponse { } , err
}
2026-05-22 16:12:52 +08:00
runner := batchImportRuntimeRunner {
store : store ,
hostRow : hostRow ,
hostClient : client ,
request : req ,
2026-05-22 15:39:43 +08:00
}
2026-05-22 16:12:52 +08:00
return runner . execute ( ctx )
2026-05-22 15:39:43 +08:00
}
}
2026-05-23 09:18:02 +08:00
func buildListBatchImportRunsAction ( sqliteDSN string ) func ( context . Context , ListBatchImportRunsRequest ) ( ListBatchImportRunsResponse , error ) {
return func ( ctx context . Context , req ListBatchImportRunsRequest ) ( ListBatchImportRunsResponse , error ) {
2026-05-22 15:39:43 +08:00
store , err := sqlite . Open ( ctx , sqliteDSN )
if err != nil {
2026-05-23 09:18:02 +08:00
return ListBatchImportRunsResponse { } , err
2026-05-22 15:39:43 +08:00
}
defer store . Close ( )
2026-05-23 09:18:02 +08:00
runs , err := store . ImportRuns ( ) . List ( ctx , 1000 )
2026-05-22 15:39:43 +08:00
if err != nil {
2026-05-23 09:18:02 +08:00
return ListBatchImportRunsResponse { } , err
2026-05-22 15:39:43 +08:00
}
2026-05-23 09:18:02 +08:00
limit := defaultPositiveInt ( req . Limit , 50 )
result := make ( [ ] batch . RunSummaryProjection , 0 , limit )
nextCursor := ( * string ) ( nil )
started := strings . TrimSpace ( req . Cursor ) == ""
2026-05-22 15:39:43 +08:00
for _ , run := range runs {
2026-05-23 09:18:02 +08:00
if ! started {
if run . RunID == strings . TrimSpace ( req . Cursor ) {
started = true
}
continue
}
2026-05-22 15:39:43 +08:00
if req . State != "" && run . State != req . State {
continue
}
if req . AccessMode != "" && run . AccessMode != req . AccessMode {
continue
}
2026-05-23 09:18:02 +08:00
if req . Query != "" {
query := strings . ToLower ( req . Query )
if ! strings . Contains ( strings . ToLower ( run . RunID ) , query ) {
items , err := store . ImportRunItems ( ) . ListByRunID ( ctx , run . RunID )
if err != nil {
return ListBatchImportRunsResponse { } , err
}
matched := false
for _ , item := range items {
if strings . Contains ( strings . ToLower ( item . ProviderID ) , query ) || strings . Contains ( strings . ToLower ( item . BaseURL ) , query ) {
matched = true
break
}
}
if ! matched {
continue
}
}
}
if len ( result ) >= limit {
cursor := run . RunID
nextCursor = & cursor
break
2026-05-22 15:39:43 +08:00
}
result = append ( result , batch . ProjectRunSummary ( run ) )
}
2026-05-23 09:18:02 +08:00
return ListBatchImportRunsResponse { Runs : result , NextCursor : nextCursor } , nil
2026-05-22 15:39:43 +08:00
}
}
func validateCreateBatchImportRunRequest ( req CreateBatchImportRunRequest ) * httpError {
if strings . TrimSpace ( req . HostID ) == "" {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "host_id is required" }
}
if strings . TrimSpace ( req . Mode ) == "" {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "mode is required" }
}
if strings . TrimSpace ( req . AccessMode ) == "" {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "access_mode is required" }
}
if len ( req . Entries ) == 0 {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "entries is required" }
}
switch strings . TrimSpace ( req . AccessMode ) {
case "subscription" :
if len ( req . SubscriptionUsers ) == 0 {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "subscription_users is required when access_mode=subscription" }
}
if req . SubscriptionDays <= 0 {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "subscription_days is required when access_mode=subscription" }
}
case "self_service" :
if strings . TrimSpace ( req . ProbeAPIKey ) == "" {
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "probe_api_key is required when access_mode=self_service" }
}
default :
return & httpError { StatusCode : http . StatusBadRequest , Code : "invalid_request" , Message : "access_mode must be subscription or self_service" }
}
return nil
}
func parsePositiveInt ( raw string ) int {
value , err := strconv . Atoi ( strings . TrimSpace ( raw ) )
if err != nil || value <= 0 {
return 0
}
return value
}
func defaultPositiveInt ( value , fallback int ) int {
if value > 0 {
return value
}
return fallback
}