package dnf90 import ( "errors" "io" "net/http" dnfcompanion "longheng.io/server/internal/modules/dnf/companion" dnfrepo "longheng.io/server/internal/modules/dnf/repository" appkit "longheng.io/server/internal/platform/app" httpx "longheng.io/server/internal/platform/httpx" "longheng.io/server/internal/services/dnfbridge" ) const companionAdminPath = "/local/companion" func registerCompanionAdmin(env *appkit.Env, bridge *dnfbridge.Service) { if env == nil || env.AdminMux == nil || bridge == nil { return } readHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 同时接受 character_id(内部/Server-90)与 characterId(GM 工具页面)。 value, ok := dnfbridge.DecodeCompanionToolCharacterID(r.URL.RawQuery) if !ok { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{ "error": "character_id (or characterId) must be a non-zero uint16", }) return } state, err := bridge.CompanionControlState(r.Context(), value) if err != nil { httpx.WriteJSON(w, companionAdminErrorStatus(err), map[string]any{ "error": err.Error(), "character_id": value, }) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{ "available": true, "mode": "next_dungeon_frozen_snapshot", "state": state, }) }) if env.AdminAuth != nil { readHandler = env.AdminAuth("platform", readHandler) } writeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 同时接受内部/Server-90 的 snake_case 与 GM 工具页面的 camelCase 协议 // (含字符串形式的角色 ID)。解析细节见 dnfbridge/companion_tool_protocol.go。 body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } request, err := dnfbridge.DecodeCompanionToolRequest(body) if err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } sources := append([]uint16(nil), request.SourceCharacterIDs...) if len(sources) == 0 && request.SourceCharacterID != 0 { sources = append(sources, request.SourceCharacterID) } if request.CharacterID == 0 || (request.Enabled && len(sources) == 0) { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{ "error": "character_id and at least one enabled source_character_id must be non-zero uint16", }) return } for _, source := range sources { if source == 0 { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{ "error": "source_character_ids must contain only non-zero uint16 values", }) return } } state, err := bridge.CompanionControlUpdateManyWithAIProfiles( r.Context(), request.CharacterID, request.ExpectedRevision, request.Enabled, sources, request.AI, request.AIProfiles, request.BalanceEnabled, request.ProgressionEnabled, request.RandomEquipmentEnabled, ) if err != nil { httpx.WriteJSON(w, companionAdminErrorStatus(err), map[string]any{ "error": err.Error(), "character_id": request.CharacterID, "expected_revision": request.ExpectedRevision, }) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{ "available": true, "mode": "next_dungeon_frozen_snapshot", "state": state, }) }) if env.AdminDangerous != nil { writeHandler = env.AdminDangerous( "platform", httpx.AdminOperationID(http.MethodPost, companionAdminPath), writeHandler, ) } else if env.AdminAuth != nil { writeHandler = env.AdminAuth("platform", writeHandler) } env.AdminMux.HandleFunc(companionAdminPath, func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: readHandler(w, r) case http.MethodPost: writeHandler(w, r) default: w.Header().Set("Allow", http.MethodGet+", "+http.MethodPost) httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) } }) } func companionAdminErrorStatus(err error) int { switch { case errors.Is(err, dnfcompanion.ErrSelection), errors.Is(err, dnfcompanion.ErrSelf): return http.StatusBadRequest case errors.Is(err, dnfcompanion.ErrRevision), errors.Is(err, dnfcompanion.ErrAccount), errors.Is(err, dnfcompanion.ErrOnline), errors.Is(err, dnfcompanion.ErrSource), errors.Is(err, dnfcompanion.ErrSnapshot), errors.Is(err, dnfrepo.ErrCompanionAccountMismatch), errors.Is(err, dnfrepo.ErrCompanionCharacterUnavailable): return http.StatusConflict case errors.Is(err, dnfrepo.ErrCompanionReadUnavailable): return http.StatusServiceUnavailable default: return http.StatusInternalServerError } }