package wup import ( "bytes" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "sort" "strings" "huya-go/internal/taf" ) // Request 是 WUP v3 请求封装。 type Request struct { Version int16 PacketType int8 Message int32 RequestID int32 Servant string Func string Timeout int32 Context map[string]string Status map[string]string NewData map[string][]byte } func NewRequest(servant, fn string, requestID int32) *Request { return &Request{ Version: 3, Servant: servant, Func: fn, RequestID: requestID, Timeout: 3000, Context: map[string]string{}, Status: map[string]string{}, NewData: map[string][]byte{}, } } func (r *Request) WriteStruct(key string, value taf.Marshaler) error { w := taf.NewWriter() if err := w.WriteStruct(0, value); err != nil { return err } r.NewData[key] = w.Bytes() return nil } func (r *Request) Encode() ([]byte, error) { data := taf.NewWriter() if err := data.WriteMapStringBytes(0, r.NewData); err != nil { return nil, err } body := taf.NewWriter() if err := body.WriteInt16(1, int64(r.Version)); err != nil { return nil, err } if err := body.WriteInt8(2, int64(r.PacketType)); err != nil { return nil, err } if err := body.WriteInt32(3, int64(r.Message)); err != nil { return nil, err } if err := body.WriteInt32(4, int64(r.RequestID)); err != nil { return nil, err } if err := body.WriteString(5, r.Servant); err != nil { return nil, err } if err := body.WriteString(6, r.Func); err != nil { return nil, err } if err := body.WriteBytes(7, data.Bytes()); err != nil { return nil, err } if err := body.WriteInt32(8, int64(r.Timeout)); err != nil { return nil, err } if err := body.WriteMapStringString(9, r.Context); err != nil { return nil, err } if err := body.WriteMapStringString(10, r.Status); err != nil { return nil, err } rawBody := body.Bytes() out := bytes.NewBuffer(make([]byte, 0, len(rawBody)+4)) var lenBuf [4]byte binary.BigEndian.PutUint32(lenBuf[:], uint32(len(rawBody)+4)) out.Write(lenBuf[:]) out.Write(rawBody) return out.Bytes(), nil } // Response 是 WUP 响应封装。 type Response struct { Version int16 PacketType int8 Message int32 RequestID int32 Servant string Func string Buffer []byte Timeout int32 Context map[string]string Status map[string]string NewData map[string][]byte } func (r *Response) Decode(data []byte) error { body := NormalizePayload(data) reader := taf.NewReader(body) r.Context = map[string]string{} r.Status = map[string]string{} r.NewData = map[string][]byte{} for { tag, typ, err := reader.ReadHead() if err != nil { if errors.Is(err, io.EOF) { return nil } return err } if tag > 10 { return nil } switch tag { case 1: v, err := reader.ReadIntValue(typ) r.Version = int16(v) if err != nil { return err } case 2: v, err := reader.ReadIntValue(typ) r.PacketType = int8(v) if err != nil { return err } case 3: v, err := reader.ReadIntValue(typ) r.Message = int32(v) if err != nil { return err } case 4: v, err := reader.ReadIntValue(typ) r.RequestID = int32(v) if err != nil { return err } case 5: v, err := reader.ReadStringValue(typ) r.Servant = v if err != nil { return err } case 6: v, err := reader.ReadStringValue(typ) r.Func = v if err != nil { return err } case 7: v, err := reader.ReadBytesValue(typ) if err != nil { return err } r.Buffer = v if err := r.decodeBuffer(); err != nil { return err } case 8: v, err := reader.ReadIntValue(typ) r.Timeout = int32(v) if err != nil { return err } case 9: if typ != taf.TypeMap { if err := reader.Skip(typ); err != nil { return err } continue } _ = reader.Seek(reader.Position() - 1) v, err := reader.ReadMapStringString(9, false) r.Context = v if err != nil { return err } case 10: if typ != taf.TypeMap { if err := reader.Skip(typ); err != nil { return err } return nil } _ = reader.Seek(reader.Position() - 1) v, err := reader.ReadMapStringString(10, false) r.Status = v if err != nil { return err } return nil default: if err := reader.Skip(typ); err != nil { return err } } } } func (r *Response) decodeBuffer() error { if len(r.Buffer) == 0 { return nil } reader := taf.NewReader(r.Buffer) m, err := reader.ReadMapStringBytes(0, false) if err != nil { return err } r.NewData = m return nil } func (r *Response) ReadStruct(key string, obj taf.Unmarshaler) (bool, error) { data := r.NewData[key] if len(data) == 0 && key == "tRsp" { data = r.NewData["tResp"] } if len(data) == 0 && key == "tResp" { data = r.NewData["tRsp"] } if len(data) == 0 { return false, nil } reader := taf.NewReader(data) _, typ, err := reader.ReadHead() if err != nil { return false, err } if typ != taf.TypeStructBegin { return false, fmt.Errorf("wup: %s 不是结构体响应: 0x%02x", key, byte(typ)) } if err := obj.ReadFrom(reader); err != nil { return false, err } return true, reader.SkipToStructEnd() } func NormalizePayload(data []byte) []byte { if len(data) < 4 { return data } n := int(binary.BigEndian.Uint32(data[:4])) if n == len(data) || n+4 == len(data) { return data[4:] } return data } // 诊断串里的取值上限:服务端错误信息可能很长,日志里只需要够判断原因的前缀。 const ( diagnoseValueLimit = 80 diagnoseFieldLimit = 6 ) // Diagnose 汇总一次 WUP 响应里可用于排障的字段。 // // 「响应缺少 tRsp/tResp」这种报错过去只有一句文案,服务端的 iRet / status / // context 全被丢掉,事后只能靠抓包复现(2026-10-16 pc-3 日志里同一账号连续 13 次 // 命中该错误,却看不出任何原因)。这里把 tag3(iRet)、tag9(context)、tag10(status) // 与 tResp 数据体的 key 一并带出来,一次日志就能定性。 func (r *Response) Diagnose() string { if r == nil { return "响应为空" } parts := []string{fmt.Sprintf("iRet=%d", r.Message)} if r.Servant != "" || r.Func != "" { parts = append(parts, fmt.Sprintf("servant=%s.%s", r.Servant, r.Func)) } if text := formatDiagnoseMap(r.Status); text != "" { parts = append(parts, "status="+text) } if text := formatDiagnoseMap(r.Context); text != "" { parts = append(parts, "context="+text) } if len(r.NewData) == 0 { parts = append(parts, "keys=空") } else { parts = append(parts, "keys="+formatDiagnoseKeys(r.NewData)) } parts = append(parts, fmt.Sprintf("payload=%dB", len(r.Buffer))) return strings.Join(parts, " ") } // formatDiagnoseMap 按键名排序输出 map,值过长时截断,保证日志稳定且不刷屏。 func formatDiagnoseMap(values map[string]string) string { if len(values) == 0 { return "" } keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for index, key := range keys { if index >= diagnoseFieldLimit { parts = append(parts, fmt.Sprintf("…(共%d项)", len(keys))) break } parts = append(parts, fmt.Sprintf("%s=%s", key, truncateDiagnoseValue(values[key]))) } return "{" + strings.Join(parts, " ") + "}" } // formatDiagnoseKeys 输出 tResp 数据体的键名(值可能是任意结构,只列键)。 func formatDiagnoseKeys(values map[string][]byte) string { keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, fmt.Sprintf("%s(%dB)", key, len(values[key]))) } sort.Strings(keys) return "{" + strings.Join(keys, " ") + "}" } func truncateDiagnoseValue(value string) string { trimmed := strings.TrimSpace(value) if len(trimmed) <= diagnoseValueLimit { return trimmed } return trimmed[:diagnoseValueLimit] + "…" } // HexHead 把二进制帧的前 n 字节转成十六进制,用于 debug 级日志定位协议问题。 func HexHead(data []byte, n int) string { if n <= 0 || len(data) == 0 { return "" } if n > len(data) { n = len(data) } return hex.EncodeToString(data[:n]) }