Files
cursor-byok/internal/backend/agent/protocol/inbound.go
2026-06-30 10:38:52 +08:00

225 lines
7.4 KiB
Go

// inbound.go 实现上行协议的解码、摘要与命令类型识别。
package protocol
import (
"encoding/hex"
"fmt"
"strings"
"unicode/utf8"
"cursor/gen/agentv1"
"cursor/gen/aiserverv1"
"cursor/internal/backend/agent/core"
"google.golang.org/protobuf/proto"
)
// ReadAppendRequestID 从 BidiAppendRequest 中读取 request_id 文本。
func ReadAppendRequestID(input *aiserverv1.BidiAppendRequest) string {
if input == nil {
return ""
}
return ReadBidiRequestID(input.GetRequestId())
}
// ReadBidiRequestID 从 BidiRequestId 结构中提取并去除首尾空白。
func ReadBidiRequestID(input *aiserverv1.BidiRequestId) string {
if input == nil {
return ""
}
return strings.TrimSpace(input.GetRequestId())
}
// NormalizeRequestID 规范化请求标识并去除首尾空白。
func NormalizeRequestID(requestID string) string {
return strings.TrimSpace(requestID)
}
// DecodeAgentClientMessage 解析 hex 文本为 AgentClientMessage,并返回消息类型标签。
func DecodeAgentClientMessage(hexData string) (*agentv1.AgentClientMessage, string, error) {
trimmed := strings.TrimSpace(hexData)
if trimmed == "" {
return nil, "", nil
}
payload, err := hex.DecodeString(trimmed)
if err != nil {
return nil, "", fmt.Errorf("bidi append data is not valid hex: %w", err)
}
clientMessage := &agentv1.AgentClientMessage{}
if err := proto.Unmarshal(payload, clientMessage); err != nil {
return nil, "", fmt.Errorf("decode agent client message failed: %w", err)
}
return clientMessage, detectClientMessageKind(clientMessage), nil
}
// MapClientMessageToCommandKind 将上行协议消息映射为运行时命令类型。
func MapClientMessageToCommandKind(message *agentv1.AgentClientMessage, clientKind string) (runtimecore.CommandKind, error) {
switch strings.TrimSpace(clientKind) {
case "run_request":
return runtimecore.CommandKindRunRequested, nil
case "prewarm_request":
return runtimecore.CommandKindPrewarmRequested, nil
case "conversation_action":
if message == nil || message.GetConversationAction() == nil {
return "", fmt.Errorf("conversation_action payload is required")
}
switch message.GetConversationAction().GetAction().(type) {
case *agentv1.ConversationAction_CancelAction:
return runtimecore.CommandKindCancelRequested, nil
case *agentv1.ConversationAction_UserMessageAction,
*agentv1.ConversationAction_ResumeAction,
*agentv1.ConversationAction_SummarizeAction,
*agentv1.ConversationAction_ShellCommandAction,
*agentv1.ConversationAction_StartPlanAction,
*agentv1.ConversationAction_ExecutePlanAction,
*agentv1.ConversationAction_AsyncAskQuestionCompletionAction,
*agentv1.ConversationAction_CancelSubagentAction,
*agentv1.ConversationAction_BackgroundShellAction,
*agentv1.ConversationAction_BackgroundTaskCompletionAction:
return runtimecore.CommandKindConversationActionRecordOnly, nil
default:
return "", fmt.Errorf("unsupported conversation_action payload")
}
case "exec_client_message":
return runtimecore.CommandKindExecClientMessage, nil
case "interaction_response":
return runtimecore.CommandKindInteractionResponse, nil
case "exec_client_control_message":
return runtimecore.CommandKindExecClientControlMessage, nil
case "client_heartbeat":
return runtimecore.CommandKindClientHeartbeat, nil
case "kv_client_message":
return runtimecore.CommandKindKVClientMessage, nil
default:
return "", fmt.Errorf("unsupported client message kind: %s", clientKind)
}
}
// IsResumeRunRequest 判断当前消息是否为带 `resume_action` 的 `run_request`。
func IsResumeRunRequest(message *agentv1.AgentClientMessage) bool {
if message == nil || message.GetRunRequest() == nil {
return false
}
action := message.GetRunRequest().GetAction()
if action == nil {
return false
}
_, ok := action.GetAction().(*agentv1.ConversationAction_ResumeAction)
return ok
}
// BuildClientHistoryEntry 将消息类型与负载摘要拼接成会话历史记录文本。
func BuildClientHistoryEntry(kind string, message *agentv1.AgentClientMessage) string {
normalizedKind := strings.TrimSpace(kind)
if normalizedKind == "" {
normalizedKind = "unknown"
}
payload := extractClientMessagePayload(message)
summary := summarizePayload(payload)
if summary == "" {
if normalizedKind == "unknown" {
return ""
}
return normalizedKind
}
return fmt.Sprintf("%s:%s", normalizedKind, summary)
}
// detectClientMessageKind 判断 oneof message 当前承载的消息分支类型。
func detectClientMessageKind(message *agentv1.AgentClientMessage) string {
if message == nil || message.GetMessage() == nil {
return ""
}
switch message.GetMessage().(type) {
case *agentv1.AgentClientMessage_RunRequest:
return "run_request"
case *agentv1.AgentClientMessage_PrewarmRequest:
return "prewarm_request"
case *agentv1.AgentClientMessage_ConversationAction:
return "conversation_action"
case *agentv1.AgentClientMessage_ExecClientMessage:
return "exec_client_message"
case *agentv1.AgentClientMessage_InteractionResponse:
return "interaction_response"
case *agentv1.AgentClientMessage_ExecClientControlMessage:
return "exec_client_control_message"
case *agentv1.AgentClientMessage_ClientHeartbeat:
return "client_heartbeat"
case *agentv1.AgentClientMessage_KvClientMessage:
return "kv_client_message"
default:
return ""
}
}
// extractClientMessagePayload 从 oneof 分支中提取原始 bytes 负载。
func extractClientMessagePayload(message *agentv1.AgentClientMessage) []byte {
if message == nil || message.GetMessage() == nil {
return nil
}
switch item := message.GetMessage().(type) {
case *agentv1.AgentClientMessage_RunRequest:
return marshalProtoMessage(item.RunRequest)
case *agentv1.AgentClientMessage_PrewarmRequest:
return marshalProtoMessage(item.PrewarmRequest)
case *agentv1.AgentClientMessage_ConversationAction:
return marshalProtoMessage(item.ConversationAction)
case *agentv1.AgentClientMessage_ExecClientMessage:
return marshalProtoMessage(item.ExecClientMessage)
case *agentv1.AgentClientMessage_InteractionResponse:
return marshalProtoMessage(item.InteractionResponse)
case *agentv1.AgentClientMessage_ExecClientControlMessage:
return marshalProtoMessage(item.ExecClientControlMessage)
case *agentv1.AgentClientMessage_ClientHeartbeat:
return marshalProtoMessage(item.ClientHeartbeat)
case *agentv1.AgentClientMessage_KvClientMessage:
return marshalProtoMessage(item.KvClientMessage)
default:
return nil
}
}
// marshalProtoMessage 将 proto 消息重新编码为 bytes,用于调试摘要展示。
func marshalProtoMessage(message proto.Message) []byte {
if message == nil {
return nil
}
payload, err := proto.Marshal(message)
if err != nil {
return nil
}
return payload
}
// summarizePayload 生成可读摘要:优先文本,无法直接读时回退为 hex 片段。
func summarizePayload(payload []byte) string {
if len(payload) == 0 {
return ""
}
if utf8.Valid(payload) {
text := strings.TrimSpace(string(payload))
text = strings.ReplaceAll(text, "\n", " ")
text = strings.ReplaceAll(text, "\r", " ")
text = strings.TrimSpace(text)
if text != "" {
return truncateText(text, 120)
}
}
return "hex:" + truncateText(hex.EncodeToString(payload), 120)
}
// truncateText 按 rune 数截断文本,避免在多字节字符中间截断导致乱码。
func truncateText(text string, maxRunes int) string {
if maxRunes <= 0 {
return ""
}
runes := []rune(text)
if len(runes) <= maxRunes {
return text
}
return string(runes[:maxRunes]) + "..."
}