package testweb import ( "encoding/base64" "encoding/json" "io" "io/fs" "net/http" "strings" ) // Deps testweb 依赖。 type Deps struct { SuperviseTarget string FileTarget string APILogRoot string } // Register 注册 /t 页面与 API。 func Register(mux *http.ServeMux, deps Deps) { s := &server{deps: deps} sub, _ := fs.Sub(webFS, "web") static := http.FileServer(http.FS(sub)) mux.HandleFunc("/t", s.handlePage) mux.HandleFunc("/t/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/t/static/") { http.StripPrefix("/t/static/", static).ServeHTTP(w, r) return } if r.URL.Path == "/t" || r.URL.Path == "/t/" { s.handlePage(w, r) return } if r.URL.Path == "/t/prescription" || r.URL.Path == "/t/prescription/" { s.handlePrescriptionPage(w, r) return } http.NotFound(w, r) }) mux.HandleFunc("/t/api/meta", s.handleMeta) mux.HandleFunc("/t/api/logs", s.handleLogs) mux.HandleFunc("/t/api/logs/detail", s.handleLogDetail) mux.HandleFunc("/t/api/logs/export", s.handleLogExport) mux.HandleFunc("/t/api/prescription/records", s.handlePrescriptionRecords) mux.HandleFunc("/t/api/prescription/rebuild", s.handlePrescriptionRebuild) mux.HandleFunc("/t/api/test/connect", s.handleTestConnect) mux.HandleFunc("/t/api/test/send", s.handleTestSend) } type server struct { deps Deps } func (s *server) handlePage(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/t" && r.URL.Path != "/t/" { http.NotFound(w, r) return } b, err := webFS.ReadFile("web/index.html") if err != nil { http.Error(w, "page not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(b) } func (s *server) handleMeta(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{ "superviseTarget": s.deps.SuperviseTarget, "fileTarget": s.deps.FileTarget, "apiLogRoot": s.deps.APILogRoot, }) } func (s *server) handleLogs(w http.ResponseWriter, _ *http.Request) { list, err := ListFileLogs(s.deps.APILogRoot) if err != nil { writeErr(w, err.Error(), http.StatusInternalServerError) return } if list == nil { list = []FileLogEntry{} } writeJSON(w, list) } func (s *server) handleLogDetail(w http.ResponseWriter, r *http.Request) { rel := r.URL.Query().Get("rel") parsed, err := ParseAPILogFile(s.deps.APILogRoot, rel) if err != nil { writeErr(w, err.Error(), http.StatusBadRequest) return } out := toDetailDTO(parsed) writeJSON(w, out) } type logDetailJSON struct { Rel string `json:"rel"` Meta map[string]string `json:"meta"` Inbound sectionDTO `json:"inbound"` Outbound sectionDTO `json:"outbound"` Response ResponseSection `json:"response"` ParseWarnings []string `json:"parseWarnings,omitempty"` OutboundAbsent bool `json:"outboundAbsent"` } type sectionDTO struct { URL string `json:"url,omitempty"` Method string `json:"method,omitempty"` Headers map[string]string `json:"headers"` BodyRaw string `json:"bodyRaw,omitempty"` BodyLen int `json:"bodyLen"` BodyBase64 string `json:"bodyBase64,omitempty"` BodyPlain bool `json:"bodyPlain"` } func toDetailDTO(p *ParsedAPILog) logDetailJSON { return logDetailJSON{ Rel: p.Rel, Meta: p.Meta, Inbound: sectionDTOFrom(p.Inbound), Outbound: sectionDTOFrom(p.Outbound), Response: p.Response, ParseWarnings: p.ParseWarnings, OutboundAbsent: p.OutboundAbsent, } } func sectionDTOFrom(sec RequestSection) sectionDTO { d := sectionDTO{ URL: sec.URL, Method: sec.Method, Headers: sec.Headers, BodyRaw: sec.BodyRaw, BodyLen: sec.BodyLen, BodyBase64: sec.BodyBase64, BodyPlain: sec.BodyPlain, } if len(sec.BodyBytes) > 0 && !sec.BodyPlain { d.BodyBase64 = base64.StdEncoding.EncodeToString(sec.BodyBytes) } return d } func (s *server) handleLogExport(w http.ResponseWriter, r *http.Request) { rel := r.URL.Query().Get("rel") useInbound := r.URL.Query().Get("source") == "inbound" parsed, err := ParseAPILogFile(s.deps.APILogRoot, rel) if err != nil { writeErr(w, err.Error(), http.StatusBadRequest) return } data, err := ToPostmanCollection(parsed, useInbound) if err != nil { writeErr(w, err.Error(), http.StatusInternalServerError) return } name := "forward-file-" + strings.ReplaceAll(rel, "/", "_") + ".postman_collection.json" w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"") _, _ = w.Write(data) } func (s *server) handleTestConnect(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req struct { Channel string `json:"channel"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, "invalid json", http.StatusBadRequest) return } ch := strings.ToLower(strings.TrimSpace(req.Channel)) var results []ConnectResult switch ch { case "supervise", "28212": results = append(results, TestConnectivity("supervise", s.deps.SuperviseTarget)) case "file", "28211": results = append(results, TestConnectivity("file", s.deps.FileTarget)) default: results = append(results, TestConnectivity("supervise", s.deps.SuperviseTarget), TestConnectivity("file", s.deps.FileTarget), ) } writeJSON(w, map[string]any{"results": results}) } func (s *server) handleTestSend(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } body, _ := io.ReadAll(r.Body) var req struct { Rel string `json:"rel"` UseOutbound bool `json:"useOutbound"` URL string `json:"url"` Headers map[string]string `json:"headers"` BodyBase64 string `json:"bodyBase64"` } if err := json.Unmarshal(body, &req); err != nil { writeErr(w, "invalid json", http.StatusBadRequest) return } url := strings.TrimSpace(req.URL) headers := req.Headers var payload []byte if req.Rel != "" { parsed, err := ParseAPILogFile(s.deps.APILogRoot, req.Rel) if err != nil { writeErr(w, err.Error(), http.StatusBadRequest) return } sec := parsed.Outbound if !req.UseOutbound { sec = parsed.Inbound } if url == "" { url = sec.URL } if len(headers) == 0 { headers = sec.Headers } payload = sec.BodyBytes } if req.BodyBase64 != "" { dec, err := base64.StdEncoding.DecodeString(req.BodyBase64) if err != nil { writeErr(w, "invalid bodyBase64", http.StatusBadRequest) return } payload = dec } if url == "" { writeErr(w, "url required", http.StatusBadRequest) return } if headers == nil { headers = map[string]string{} } result := ReplayRequest(url, headers, payload) writeJSON(w, result) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(v) } func writeErr(w http.ResponseWriter, msg string, code int) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) }