mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
492 lines
14 KiB
Go
492 lines
14 KiB
Go
package forwarder
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
"cursor/gen/agentv1"
|
|
runtimecore "cursor/internal/backend/agent/core"
|
|
)
|
|
|
|
const editReadBinaryContentLimit = 32 * 1024
|
|
|
|
type editComputation struct {
|
|
BeforeContent string
|
|
AfterContent string
|
|
DiffString string
|
|
LinesAdded int32
|
|
LinesRemoved int32
|
|
Message string
|
|
}
|
|
|
|
type editResultPayload struct {
|
|
BeforeContent string
|
|
AfterContent string
|
|
DiffString string
|
|
LinesAdded int32
|
|
LinesRemoved int32
|
|
Message string
|
|
}
|
|
|
|
type editMatch struct {
|
|
Start int
|
|
End int
|
|
}
|
|
|
|
type normalizedEditContent struct {
|
|
Text string
|
|
OriginalOffsets []int
|
|
}
|
|
|
|
func computeEditDiff(beforeContent string, afterContent string) (string, int32, int32) {
|
|
dmp := diffmatchpatch.New()
|
|
charsBefore, charsAfter, lineArray := dmp.DiffLinesToChars(beforeContent, afterContent)
|
|
diffs := dmp.DiffMain(charsBefore, charsAfter, false)
|
|
diffs = dmp.DiffCharsToLines(diffs, lineArray)
|
|
|
|
linesAdded := int32(0)
|
|
linesRemoved := int32(0)
|
|
for _, diff := range diffs {
|
|
switch diff.Type {
|
|
case diffmatchpatch.DiffInsert:
|
|
linesAdded += countChangedLines(diff.Text)
|
|
case diffmatchpatch.DiffDelete:
|
|
linesRemoved += countChangedLines(diff.Text)
|
|
}
|
|
}
|
|
return dmp.PatchToText(dmp.PatchMake(beforeContent, diffs)), linesAdded, linesRemoved
|
|
}
|
|
|
|
func normalizeEditContentLineEndings(content string) normalizedEditContent {
|
|
var builder strings.Builder
|
|
builder.Grow(len(content))
|
|
offsets := make([]int, 1, len(content)+1)
|
|
offsets[0] = 0
|
|
for index := 0; index < len(content); {
|
|
if content[index] == '\r' && index+1 < len(content) && content[index+1] == '\n' {
|
|
builder.WriteByte('\n')
|
|
index += 2
|
|
offsets = append(offsets, index)
|
|
continue
|
|
}
|
|
if content[index] == '\r' {
|
|
builder.WriteByte('\n')
|
|
index++
|
|
offsets = append(offsets, index)
|
|
continue
|
|
}
|
|
builder.WriteByte(content[index])
|
|
index++
|
|
offsets = append(offsets, index)
|
|
}
|
|
return normalizedEditContent{
|
|
Text: builder.String(),
|
|
OriginalOffsets: offsets,
|
|
}
|
|
}
|
|
|
|
func normalizeLineEndingsToLF(value string) string {
|
|
if !strings.ContainsAny(value, "\r\n") {
|
|
return value
|
|
}
|
|
normalized := strings.ReplaceAll(value, "\r\n", "\n")
|
|
return strings.ReplaceAll(normalized, "\r", "\n")
|
|
}
|
|
|
|
func detectPreferredFileLineEnding(content string) string {
|
|
return dominantLineEnding(content, "\n")
|
|
}
|
|
|
|
func preferredMatchLineEnding(content string, match editMatch, defaultLineEnding string) string {
|
|
return dominantLineEnding(content[match.Start:match.End], defaultLineEnding)
|
|
}
|
|
|
|
func normalizeReplacementLineEndings(newString string, lineEnding string) string {
|
|
if !strings.ContainsAny(newString, "\r\n") {
|
|
return newString
|
|
}
|
|
normalized := normalizeLineEndingsToLF(newString)
|
|
switch lineEnding {
|
|
case "\r\n":
|
|
return strings.ReplaceAll(normalized, "\n", "\r\n")
|
|
case "\r":
|
|
return strings.ReplaceAll(normalized, "\n", "\r")
|
|
default:
|
|
return normalized
|
|
}
|
|
}
|
|
|
|
func dominantLineEnding(content string, fallback string) string {
|
|
if fallback == "" {
|
|
fallback = "\n"
|
|
}
|
|
counts := map[string]int{
|
|
"\n": 0,
|
|
"\r\n": 0,
|
|
"\r": 0,
|
|
}
|
|
first := ""
|
|
for index := 0; index < len(content); {
|
|
switch {
|
|
case content[index] == '\r' && index+1 < len(content) && content[index+1] == '\n':
|
|
counts["\r\n"]++
|
|
if first == "" {
|
|
first = "\r\n"
|
|
}
|
|
index += 2
|
|
case content[index] == '\r':
|
|
counts["\r"]++
|
|
if first == "" {
|
|
first = "\r"
|
|
}
|
|
index++
|
|
case content[index] == '\n':
|
|
counts["\n"]++
|
|
if first == "" {
|
|
first = "\n"
|
|
}
|
|
index++
|
|
default:
|
|
index++
|
|
}
|
|
}
|
|
best := ""
|
|
bestCount := 0
|
|
for _, candidate := range []string{"\r\n", "\n", "\r"} {
|
|
if counts[candidate] > bestCount {
|
|
best = candidate
|
|
bestCount = counts[candidate]
|
|
}
|
|
}
|
|
if bestCount == 0 {
|
|
return fallback
|
|
}
|
|
for _, candidate := range []string{"\r\n", "\n", "\r"} {
|
|
if counts[candidate] == bestCount && candidate == first {
|
|
return candidate
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func countChangedLines(text string) int32 {
|
|
if text == "" {
|
|
return 0
|
|
}
|
|
count := int32(strings.Count(text, "\n"))
|
|
if !strings.HasSuffix(text, "\n") {
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
func buildCompletedEditToolCall(path string, result *agentv1.EditResult) *agentv1.ToolCall {
|
|
args := &agentv1.EditArgs{Path: strings.TrimSpace(path)}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_EditToolCall{
|
|
EditToolCall: &agentv1.EditToolCall{
|
|
Args: args,
|
|
Result: result,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildSuccessfulEditResult(path string, beforeContent string, afterContent string, diffString string, linesAdded int32, linesRemoved int32, message string) *agentv1.EditResult {
|
|
success := &agentv1.EditSuccess{
|
|
Path: strings.TrimSpace(path),
|
|
AfterFullFileContent: afterContent,
|
|
}
|
|
if beforeContent != "" {
|
|
success.BeforeFullFileContent = stringPtr(beforeContent)
|
|
}
|
|
if diffString != "" {
|
|
success.DiffString = stringPtr(diffString)
|
|
}
|
|
if linesAdded != 0 {
|
|
success.LinesAdded = int32Ptr(linesAdded)
|
|
}
|
|
if linesRemoved != 0 {
|
|
success.LinesRemoved = int32Ptr(linesRemoved)
|
|
}
|
|
if message != "" {
|
|
success.Message = stringPtr(message)
|
|
}
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_Success{Success: success},
|
|
}
|
|
}
|
|
|
|
func buildFinalEditSuccessResult(path string, afterContent string, payload editResultPayload) *agentv1.EditResult {
|
|
diffString := payload.DiffString
|
|
linesAdded := payload.LinesAdded
|
|
linesRemoved := payload.LinesRemoved
|
|
if afterContent != payload.AfterContent {
|
|
diffString, linesAdded, linesRemoved = computeEditDiff(payload.BeforeContent, afterContent)
|
|
}
|
|
return buildSuccessfulEditResult(path, payload.BeforeContent, afterContent, diffString, linesAdded, linesRemoved, payload.Message)
|
|
}
|
|
|
|
func buildEditErrorResult(path string, errorMessage string) *agentv1.EditResult {
|
|
trimmedPath := strings.TrimSpace(path)
|
|
trimmedError := strings.TrimSpace(errorMessage)
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_Error{
|
|
Error: &agentv1.EditError{
|
|
Path: trimmedPath,
|
|
Error: trimmedError,
|
|
ModelVisibleError: stringPtr(trimmedError),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func decodeReadDataAsEditableText(data []byte) (string, bool, string) {
|
|
if len(data) > editReadBinaryContentLimit {
|
|
return "", false, fmt.Sprintf("Read returned binary data larger than %d bytes", editReadBinaryContentLimit)
|
|
}
|
|
if !utf8.Valid(data) {
|
|
return "", false, "Read returned binary data that is not valid UTF-8 text"
|
|
}
|
|
return string(data), true, ""
|
|
}
|
|
|
|
func buildEditResultFromReadResult(path string, result *agentv1.ReadResult) *agentv1.EditResult {
|
|
if result == nil {
|
|
return buildEditErrorResult(path, "read result missing")
|
|
}
|
|
switch item := result.GetResult().(type) {
|
|
case *agentv1.ReadResult_FileNotFound:
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_FileNotFound{
|
|
FileNotFound: &agentv1.EditFileNotFound{Path: firstNonEmpty(item.FileNotFound.GetPath(), path)},
|
|
},
|
|
}
|
|
case *agentv1.ReadResult_PermissionDenied:
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_ReadPermissionDenied{
|
|
ReadPermissionDenied: &agentv1.EditReadPermissionDenied{Path: firstNonEmpty(item.PermissionDenied.GetPath(), path)},
|
|
},
|
|
}
|
|
case *agentv1.ReadResult_Rejected:
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_Rejected{
|
|
Rejected: &agentv1.EditRejected{
|
|
Path: firstNonEmpty(item.Rejected.GetPath(), path),
|
|
Reason: item.Rejected.GetReason(),
|
|
},
|
|
},
|
|
}
|
|
case *agentv1.ReadResult_InvalidFile:
|
|
return buildEditErrorResult(firstNonEmpty(item.InvalidFile.GetPath(), path), item.InvalidFile.GetReason())
|
|
case *agentv1.ReadResult_Error:
|
|
return buildEditErrorResult(firstNonEmpty(item.Error.GetPath(), path), item.Error.GetError())
|
|
case *agentv1.ReadResult_Success:
|
|
return buildEditErrorResult(firstNonEmpty(item.Success.GetPath(), path), "read result did not include editable text content")
|
|
default:
|
|
return buildEditErrorResult(path, "unknown read result")
|
|
}
|
|
}
|
|
|
|
func buildEditResultFromWriteResult(path string, result *agentv1.WriteResult) *agentv1.EditResult {
|
|
if result == nil {
|
|
return buildEditErrorResult(path, "write result missing")
|
|
}
|
|
switch item := result.GetResult().(type) {
|
|
case *agentv1.WriteResult_PermissionDenied:
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_WritePermissionDenied{
|
|
WritePermissionDenied: &agentv1.EditWritePermissionDenied{
|
|
Path: firstNonEmpty(item.PermissionDenied.GetPath(), path),
|
|
Error: item.PermissionDenied.GetError(),
|
|
},
|
|
},
|
|
}
|
|
case *agentv1.WriteResult_Rejected:
|
|
return &agentv1.EditResult{
|
|
Result: &agentv1.EditResult_Rejected{
|
|
Rejected: &agentv1.EditRejected{
|
|
Path: firstNonEmpty(item.Rejected.GetPath(), path),
|
|
Reason: item.Rejected.GetReason(),
|
|
},
|
|
},
|
|
}
|
|
case *agentv1.WriteResult_NoSpace:
|
|
return buildEditErrorResult(firstNonEmpty(item.NoSpace.GetPath(), path), "no space left")
|
|
case *agentv1.WriteResult_Error:
|
|
return buildEditErrorResult(firstNonEmpty(item.Error.GetPath(), path), item.Error.GetError())
|
|
case *agentv1.WriteResult_Success:
|
|
afterContent := item.Success.GetFileContentAfterWrite()
|
|
return buildSuccessfulEditResult(firstNonEmpty(item.Success.GetPath(), path), "", afterContent, "", 0, 0, "")
|
|
default:
|
|
return buildEditErrorResult(path, "unknown write result")
|
|
}
|
|
}
|
|
|
|
func extractReadContentForEdit(result *agentv1.ReadResult) (string, bool) {
|
|
success := result.GetSuccess()
|
|
if success == nil {
|
|
return "", false
|
|
}
|
|
switch output := success.GetOutput().(type) {
|
|
case *agentv1.ReadSuccess_Content:
|
|
return normalizeLineEndingsToLF(output.Content), true
|
|
case *agentv1.ReadSuccess_Data:
|
|
text, ok, _ := decodeReadDataAsEditableText(output.Data)
|
|
return normalizeLineEndingsToLF(text), ok
|
|
}
|
|
if success.GetOutputBlobId() != nil {
|
|
return "", false
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
func summarizeEditResult(result *agentv1.EditResult) string {
|
|
if result == nil {
|
|
return "edit result missing"
|
|
}
|
|
switch item := result.GetResult().(type) {
|
|
case *agentv1.EditResult_Success:
|
|
return item.Success.GetAfterFullFileContent()
|
|
case *agentv1.EditResult_FileNotFound:
|
|
return fmt.Sprintf("file not found: %s", item.FileNotFound.GetPath())
|
|
case *agentv1.EditResult_ReadPermissionDenied:
|
|
return fmt.Sprintf("permission denied: %s", item.ReadPermissionDenied.GetPath())
|
|
case *agentv1.EditResult_WritePermissionDenied:
|
|
return item.WritePermissionDenied.GetError()
|
|
case *agentv1.EditResult_Rejected:
|
|
return item.Rejected.GetReason()
|
|
case *agentv1.EditResult_Error:
|
|
return item.Error.GetError()
|
|
default:
|
|
return "unknown edit result"
|
|
}
|
|
}
|
|
|
|
func summarizeEditResultWithoutPath(result *agentv1.EditResult) string {
|
|
if result == nil {
|
|
return "edit result missing"
|
|
}
|
|
switch item := result.GetResult().(type) {
|
|
case *agentv1.EditResult_Success:
|
|
return item.Success.GetAfterFullFileContent()
|
|
case *agentv1.EditResult_FileNotFound:
|
|
return "file not found"
|
|
case *agentv1.EditResult_ReadPermissionDenied:
|
|
return "permission denied"
|
|
case *agentv1.EditResult_WritePermissionDenied:
|
|
return item.WritePermissionDenied.GetError()
|
|
case *agentv1.EditResult_Rejected:
|
|
return item.Rejected.GetReason()
|
|
case *agentv1.EditResult_Error:
|
|
return item.Error.GetError()
|
|
default:
|
|
return "unknown edit result"
|
|
}
|
|
}
|
|
|
|
func editResultWithoutPath(result *agentv1.EditResult) *agentv1.EditResult {
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
switch item := result.GetResult().(type) {
|
|
case *agentv1.EditResult_Success:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_Success{Success: &agentv1.EditSuccess{
|
|
AfterFullFileContent: item.Success.GetAfterFullFileContent(),
|
|
BeforeFullFileContent: item.Success.BeforeFullFileContent,
|
|
DiffString: item.Success.DiffString,
|
|
LinesAdded: item.Success.LinesAdded,
|
|
LinesRemoved: item.Success.LinesRemoved,
|
|
Message: item.Success.Message,
|
|
}}}
|
|
case *agentv1.EditResult_FileNotFound:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_FileNotFound{FileNotFound: &agentv1.EditFileNotFound{}}}
|
|
case *agentv1.EditResult_ReadPermissionDenied:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_ReadPermissionDenied{ReadPermissionDenied: &agentv1.EditReadPermissionDenied{}}}
|
|
case *agentv1.EditResult_WritePermissionDenied:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_WritePermissionDenied{WritePermissionDenied: &agentv1.EditWritePermissionDenied{Error: item.WritePermissionDenied.GetError()}}}
|
|
case *agentv1.EditResult_Rejected:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_Rejected{Rejected: &agentv1.EditRejected{Reason: item.Rejected.GetReason()}}}
|
|
case *agentv1.EditResult_Error:
|
|
return &agentv1.EditResult{Result: &agentv1.EditResult_Error{Error: &agentv1.EditError{
|
|
Error: item.Error.GetError(),
|
|
ModelVisibleError: item.Error.ModelVisibleError,
|
|
}}}
|
|
default:
|
|
return result
|
|
}
|
|
}
|
|
|
|
func summarizeStructuredEditResult(result *agentv1.EditResult) string {
|
|
if result == nil {
|
|
return "edit result missing"
|
|
}
|
|
if _, ok := result.GetResult().(*agentv1.EditResult_Success); ok {
|
|
encoded, err := protojson.Marshal(result)
|
|
if err == nil {
|
|
return string(encoded)
|
|
}
|
|
}
|
|
return summarizeEditResult(result)
|
|
}
|
|
|
|
func hiddenWriteControlError(message *agentv1.ExecClientControlMessage) string {
|
|
if message == nil {
|
|
return "write operation failed"
|
|
}
|
|
switch item := message.GetMessage().(type) {
|
|
case *agentv1.ExecClientControlMessage_Throw:
|
|
return firstNonEmpty(strings.TrimSpace(item.Throw.GetError()), "write operation failed")
|
|
case *agentv1.ExecClientControlMessage_StreamClose:
|
|
return "write operation closed unexpectedly"
|
|
default:
|
|
return "write operation failed"
|
|
}
|
|
}
|
|
|
|
func readJSONStringAny(args map[string]any, keys ...string) (string, bool, bool) {
|
|
for _, key := range keys {
|
|
value, ok := args[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
typed, ok := value.(string)
|
|
if !ok {
|
|
return "", true, false
|
|
}
|
|
return typed, true, true
|
|
}
|
|
return "", false, false
|
|
}
|
|
|
|
func readJSONIntAny(args map[string]any, keys ...string) (int, bool, bool) {
|
|
value, found, err := runtimecore.ReadIntArg(args, keys...)
|
|
if err != nil {
|
|
return 0, true, false
|
|
}
|
|
return value, found, found
|
|
}
|
|
|
|
func readJSONBoolAny(args map[string]any, keys ...string) (bool, bool, bool) {
|
|
for _, key := range keys {
|
|
value, ok := args[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
typed, ok := value.(bool)
|
|
if !ok {
|
|
return false, true, false
|
|
}
|
|
return typed, true, true
|
|
}
|
|
return false, false, false
|
|
}
|
|
|
|
func int32Ptr(value int32) *int32 {
|
|
return &value
|
|
}
|