Files
cursor-byok/cursor-proxy-debugger/decode.go
T
leokun 1854a3ab9a feat(cursor): add independent Cursor protocol debugging proxy and build tasks
- Introduced a new task for running the Cursor protocol debugging proxy.
- Added a build task for the Cursor protocol debugging proxy.
- Updated documentation to include usage instructions for the debugging tool.
2026-08-03 00:19:18 +08:00

237 lines
6.4 KiB
Go

package proxydebugger
import (
"bytes"
"compress/gzip"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"strings"
"cursor/gen/agentv1"
"cursor/gen/aiserverv1"
agentprotocol "cursor/internal/backend/agent/protocol"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
const maxConnectFrameBytes = 64 << 20
type connectFrameDecoder struct {
buffer []byte
messageType string
codec string
maxFrames int
frameCount int
onFrame func(FrameView)
}
func newConnectFrameDecoder(messageType string, codec string, maxFrames int, onFrame func(FrameView)) *connectFrameDecoder {
return &connectFrameDecoder{
messageType: messageType,
codec: strings.TrimSpace(codec),
maxFrames: maxFrames,
onFrame: onFrame,
}
}
func (decoder *connectFrameDecoder) Write(payload []byte) {
if len(payload) == 0 || decoder.frameCount >= decoder.maxFrames {
return
}
decoder.buffer = append(decoder.buffer, payload...)
for len(decoder.buffer) >= 5 && decoder.frameCount < decoder.maxFrames {
flags := decoder.buffer[0]
length := int(binary.BigEndian.Uint32(decoder.buffer[1:5]))
if length < 0 || length > maxConnectFrameBytes {
decoder.emit(FrameView{Flags: flags, Length: length, Error: "Connect 帧长度异常"})
decoder.buffer = nil
return
}
if len(decoder.buffer) < 5+length {
return
}
framePayload := append([]byte(nil), decoder.buffer[5:5+length]...)
decoder.buffer = decoder.buffer[5+length:]
decoder.emit(decoder.decode(flags, framePayload))
}
}
func (decoder *connectFrameDecoder) Close() {
if len(decoder.buffer) > 0 && decoder.frameCount < decoder.maxFrames {
decoder.emit(FrameView{
Length: len(decoder.buffer),
RawHex: clippedHex(decoder.buffer, 4096),
Error: "流结束时仍有不完整的 Connect 帧",
})
}
decoder.buffer = nil
}
func (decoder *connectFrameDecoder) emit(frame FrameView) {
frame.Index = decoder.frameCount
decoder.frameCount++
if decoder.onFrame != nil {
decoder.onFrame(frame)
}
}
func (decoder *connectFrameDecoder) decode(flags uint8, payload []byte) FrameView {
frame := FrameView{
Flags: flags,
Length: len(payload),
Compressed: flags&0x01 != 0,
EndStream: flags&0x02 != 0,
RawHex: clippedHex(payload, 4096),
}
decoded := payload
if frame.Compressed {
var err error
decoded, err = decompressPayload(payload, decoder.codec)
if err != nil {
frame.Error = err.Error()
return frame
}
}
if frame.EndStream {
frame.Kind = "end_stream"
frame.MessageType = "connect.error.v1.EndStreamResponse"
frame.JSON = prettyJSON(decoded)
return frame
}
message := newMessage(decoder.messageType)
if message == nil {
frame.Error = "未知的 protobuf 消息类型"
return frame
}
if err := proto.Unmarshal(decoded, message); err != nil {
frame.Error = fmt.Sprintf("protobuf 解码失败:%v", err)
return frame
}
frame.MessageType = decoder.messageType
frame.Kind = activeOneofName(message)
if requestID, ok := message.(*aiserverv1.BidiRequestId); ok {
frame.RequestID = strings.TrimSpace(requestID.GetRequestId())
}
frame.JSON = marshalProtoJSON(message)
return frame
}
func decompressPayload(payload []byte, codec string) ([]byte, error) {
if codec != "" && !strings.EqualFold(codec, "gzip") {
return nil, fmt.Errorf("暂不支持压缩算法 %q", codec)
}
reader, err := gzip.NewReader(bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("gzip 解压失败:%w", err)
}
defer reader.Close()
decoded, err := io.ReadAll(io.LimitReader(reader, maxConnectFrameBytes+1))
if err != nil {
return nil, fmt.Errorf("读取 gzip 内容失败:%w", err)
}
if len(decoded) > maxConnectFrameBytes {
return nil, fmt.Errorf("gzip 解压后超过 %d 字节限制", maxConnectFrameBytes)
}
return decoded, nil
}
func decodeUnary(path string, payload []byte) (decodedJSON string, kind string, requestID string, err error) {
var message proto.Message
switch path {
case "/aiserver.v1.BidiService/BidiAppend":
request := &aiserverv1.BidiAppendRequest{}
if err := proto.Unmarshal(payload, request); err != nil {
return "", "", "", err
}
requestID := strings.TrimSpace(request.GetRequestId().GetRequestId())
outer := marshalProtoJSON(request)
clientMessage, clientKind, decodeErr := agentprotocol.DecodeAgentClientMessage(request.GetData())
if decodeErr != nil || clientMessage == nil {
return outer, "bidi_append", requestID, decodeErr
}
combined := struct {
BidiAppendRequest json.RawMessage `json:"bidi_append_request"`
AgentClientKind string `json:"agent_client_kind"`
AgentClient json.RawMessage `json:"agent_client_message"`
}{
BidiAppendRequest: json.RawMessage(outer),
AgentClientKind: clientKind,
AgentClient: json.RawMessage(marshalProtoJSON(clientMessage)),
}
formatted, marshalErr := json.MarshalIndent(combined, "", " ")
return string(formatted), clientKind, requestID, marshalErr
default:
message = nil
}
if message == nil {
return "", "", "", nil
}
return marshalProtoJSON(message), activeOneofName(message), "", nil
}
func newMessage(messageType string) proto.Message {
switch messageType {
case "aiserver.v1.BidiRequestId":
return &aiserverv1.BidiRequestId{}
case "agent.v1.AgentServerMessage":
return &agentv1.AgentServerMessage{}
default:
return nil
}
}
func marshalProtoJSON(message proto.Message) string {
if message == nil {
return ""
}
payload, err := (protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: false,
Indent: " ",
}).Marshal(message)
if err != nil {
return ""
}
return string(payload)
}
func activeOneofName(message proto.Message) string {
if message == nil {
return ""
}
reflected := message.ProtoReflect()
oneofs := reflected.Descriptor().Oneofs()
for index := 0; index < oneofs.Len(); index++ {
oneof := oneofs.Get(index)
field := reflected.WhichOneof(oneof)
if field != nil {
return string(field.Name())
}
}
return string(reflected.Descriptor().Name())
}
func prettyJSON(payload []byte) string {
var target any
if err := json.Unmarshal(payload, &target); err != nil {
return string(payload)
}
formatted, err := json.MarshalIndent(target, "", " ")
if err != nil {
return string(payload)
}
return string(formatted)
}
func clippedHex(payload []byte, max int) string {
if len(payload) > max {
return hex.EncodeToString(payload[:max]) + "..."
}
return hex.EncodeToString(payload)
}