// bridge.go 实现 MVP 阶段的交互桥协议映射。 package interaction import ( "bytes" "encoding/json" "fmt" "html" "io" "mime" "net" "net/http" neturl "net/url" "regexp" "strings" "sync/atomic" "time" "unicode/utf8" readability "codeberg.org/readeck/go-readability/v2" htmlmarkdown "github.com/firecrawl/html-to-markdown" mdplugin "github.com/firecrawl/html-to-markdown/plugin" "cursor/gen/agentv1" "cursor/internal/backend/agent/core" "cursor/internal/netproxy" ) // InteractionApplyResult 表示一次交互桥结果归一化后的最小产物。 type InteractionApplyResult struct { // ToolCallID 表示该结果所属工具调用标识。 ToolCallID string // InteractionID 表示该结果所属交互桥标识。 InteractionID string // IsTerminal 表示交互桥是否已经收口。 IsTerminal bool // ToolResultPayload 表示可继续喂给模型的结果摘要。 ToolResultPayload string // ToolCall 保存可用于发 ToolCallCompletedUpdate 的工具调用对象。 ToolCall *agentv1.ToolCall } // InteractionBridge 定义交互桥接口。 type InteractionBridge interface { // OpenQuery 打开一条交互型工具调用。 OpenQuery(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) // ApplyInteractionResponse 处理交互响应。 ApplyInteractionResponse(msg *agentv1.InteractionResponse, pending runtimecore.PendingInteraction) (InteractionApplyResult, error) } // Bridge 实现 MVP 阶段的交互桥。 type Bridge struct { // nextID 生成交互消息编号。 nextID atomic.Uint32 // httpClient 负责执行 web search / web fetch 等需要外网的操作。 httpClient *http.Client } // NewBridge 创建一个交互桥实例。 func NewBridge() *Bridge { return &Bridge{ httpClient: netproxy.NewHTTPClient(15 * time.Second), } } // OpenQuery 打开一条交互型工具调用。 func (bridge *Bridge) OpenQuery(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { switch toolCall.ToolName { case "AskQuestion": return bridge.openAskQuestion(toolCall) case "CreatePlan": return bridge.openCreatePlan(toolCall) case "WebSearch": return bridge.openWebSearch(toolCall) case "WebFetch": return bridge.openWebFetch(toolCall) case "SwitchMode": return bridge.openSwitchMode(toolCall) default: return nil, runtimecore.PendingInteraction{}, fmt.Errorf("unsupported interaction tool: %s", toolCall.ToolName) } } // ApplyInteractionResponse 处理交互响应。 func (bridge *Bridge) ApplyInteractionResponse(msg *agentv1.InteractionResponse, pending runtimecore.PendingInteraction) (InteractionApplyResult, error) { if msg == nil { return InteractionApplyResult{}, fmt.Errorf("interaction response is required") } result := InteractionApplyResult{ ToolCallID: pending.ToolCallID, InteractionID: pending.InteractionID, IsTerminal: true, } switch pending.InteractionKind { case "ask_question": var args agentv1.AskQuestionArgs _ = json.Unmarshal(pending.ArgsJSON, &args) result.ToolResultPayload = summarizeAskQuestionResponse(msg.GetAskQuestionInteractionResponse()) result.ToolCall = &agentv1.ToolCall{ Tool: &agentv1.ToolCall_AskQuestionToolCall{ AskQuestionToolCall: &agentv1.AskQuestionToolCall{ Args: &args, Result: msg.GetAskQuestionInteractionResponse().GetResult(), }, }, } return result, nil case "create_plan": args, err := runtimecore.DecodeCreatePlanArgsJSON(pending.ArgsJSON) if err != nil { args = &agentv1.CreatePlanArgs{} } createPlanResult := normalizeCreatePlanResult(msg.GetCreatePlanRequestResponse()) result.ToolResultPayload = summarizeCreatePlanResult(createPlanResult) result.ToolCall = &agentv1.ToolCall{ Tool: &agentv1.ToolCall_CreatePlanToolCall{ CreatePlanToolCall: &agentv1.CreatePlanToolCall{ Args: args, Result: createPlanResult, }, }, } return result, nil case "web_search": var args agentv1.WebSearchArgs _ = json.Unmarshal(pending.ArgsJSON, &args) webSearchResult, payload := bridge.applyWebSearchResponse(msg.GetWebSearchRequestResponse(), &args) result.ToolResultPayload = payload result.ToolCall = &agentv1.ToolCall{ Tool: &agentv1.ToolCall_WebSearchToolCall{ WebSearchToolCall: &agentv1.WebSearchToolCall{ Args: &args, Result: webSearchResult, }, }, } return result, nil case "web_fetch": var args agentv1.WebFetchArgs _ = json.Unmarshal(pending.ArgsJSON, &args) webFetchResult, payload := bridge.applyWebFetchResponse(msg.GetWebFetchRequestResponse(), &args) result.ToolResultPayload = payload result.ToolCall = &agentv1.ToolCall{ Tool: &agentv1.ToolCall_WebFetchToolCall{ WebFetchToolCall: &agentv1.WebFetchToolCall{ Args: &args, Result: webFetchResult, }, }, } return result, nil case "switch_mode": var args agentv1.SwitchModeArgs _ = json.Unmarshal(pending.ArgsJSON, &args) switchModeResult := buildSwitchModeResult(msg.GetSwitchModeRequestResponse(), &args) result.ToolResultPayload = summarizeSwitchModeResponse(switchModeResult) result.ToolCall = &agentv1.ToolCall{ Tool: &agentv1.ToolCall_SwitchModeToolCall{ SwitchModeToolCall: &agentv1.SwitchModeToolCall{ Args: &args, Result: switchModeResult, }, }, } return result, nil default: return InteractionApplyResult{}, fmt.Errorf("unsupported pending interaction kind: %s", pending.InteractionKind) } } // nextMessageID 返回下一个交互消息编号。 func (bridge *Bridge) nextMessageID() uint32 { current := bridge.nextID.Add(1) if current == 0 { current = bridge.nextID.Add(1) } return current } // openAskQuestion 构造 AskQuestion 交互查询。 func (bridge *Bridge) openAskQuestion(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { var args agentv1.AskQuestionArgs if err := json.Unmarshal(toolCall.ArgsJSON, &args); err != nil { return nil, runtimecore.PendingInteraction{}, fmt.Errorf("decode AskQuestion args failed: %w", err) } messageID := bridge.nextMessageID() serverMessage := &agentv1.AgentServerMessage{ Message: &agentv1.AgentServerMessage_InteractionQuery{ InteractionQuery: &agentv1.InteractionQuery{ Id: messageID, Query: &agentv1.InteractionQuery_AskQuestionInteractionQuery{ AskQuestionInteractionQuery: &agentv1.AskQuestionInteractionQuery{ Args: &args, ToolCallId: toolCall.CallID, }, }, }, }, } return serverMessage, runtimecore.PendingInteraction{ InteractionID: fmt.Sprintf("%d", messageID), ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...), ToolCallID: toolCall.CallID, InteractionKind: "ask_question", }, nil } // openCreatePlan 构造 CreatePlan 交互查询。 func (bridge *Bridge) openCreatePlan(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { args, err := runtimecore.DecodeCreatePlanArgsJSON(toolCall.ArgsJSON) if err != nil { return nil, runtimecore.PendingInteraction{}, fmt.Errorf("decode CreatePlan args failed: %w", err) } messageID := bridge.nextMessageID() serverMessage := &agentv1.AgentServerMessage{ Message: &agentv1.AgentServerMessage_InteractionQuery{ InteractionQuery: &agentv1.InteractionQuery{ Id: messageID, Query: &agentv1.InteractionQuery_CreatePlanRequestQuery{ CreatePlanRequestQuery: &agentv1.CreatePlanRequestQuery{ Args: args, ToolCallId: toolCall.CallID, }, }, }, }, } return serverMessage, runtimecore.PendingInteraction{ InteractionID: fmt.Sprintf("%d", messageID), ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...), ToolCallID: toolCall.CallID, InteractionKind: "create_plan", }, nil } // openWebSearch 构造 WebSearch 交互查询。 func (bridge *Bridge) openWebSearch(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { var input struct { SearchTerm string `json:"search_term"` } if err := json.Unmarshal(toolCall.ArgsJSON, &input); err != nil { return nil, runtimecore.PendingInteraction{}, fmt.Errorf("decode WebSearch args failed: %w", err) } messageID := bridge.nextMessageID() serverMessage := &agentv1.AgentServerMessage{ Message: &agentv1.AgentServerMessage_InteractionQuery{ InteractionQuery: &agentv1.InteractionQuery{ Id: messageID, Query: &agentv1.InteractionQuery_WebSearchRequestQuery{ WebSearchRequestQuery: &agentv1.WebSearchRequestQuery{ Args: &agentv1.WebSearchArgs{ SearchTerm: input.SearchTerm, ToolCallId: toolCall.CallID, }, }, }, }, }, } return serverMessage, runtimecore.PendingInteraction{ InteractionID: fmt.Sprintf("%d", messageID), ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...), ToolCallID: toolCall.CallID, InteractionKind: "web_search", }, nil } // openWebFetch 构造 WebFetch 交互查询。 func (bridge *Bridge) openWebFetch(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { var input struct { URL string `json:"url"` } if err := json.Unmarshal(toolCall.ArgsJSON, &input); err != nil { return nil, runtimecore.PendingInteraction{}, fmt.Errorf("decode WebFetch args failed: %w", err) } messageID := bridge.nextMessageID() serverMessage := &agentv1.AgentServerMessage{ Message: &agentv1.AgentServerMessage_InteractionQuery{ InteractionQuery: &agentv1.InteractionQuery{ Id: messageID, Query: &agentv1.InteractionQuery_WebFetchRequestQuery{ WebFetchRequestQuery: &agentv1.WebFetchRequestQuery{ Args: &agentv1.WebFetchArgs{ Url: input.URL, ToolCallId: toolCall.CallID, }, }, }, }, }, } argsPayload, _ := json.Marshal(agentv1.WebFetchArgs{ Url: input.URL, ToolCallId: toolCall.CallID, }) return serverMessage, runtimecore.PendingInteraction{ InteractionID: fmt.Sprintf("%d", messageID), ArgsJSON: argsPayload, ToolCallID: toolCall.CallID, InteractionKind: "web_fetch", }, nil } // openSwitchMode 构造 SwitchMode 交互查询。 func (bridge *Bridge) openSwitchMode(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingInteraction, error) { var args agentv1.SwitchModeArgs if err := json.Unmarshal(toolCall.ArgsJSON, &args); err != nil { return nil, runtimecore.PendingInteraction{}, fmt.Errorf("decode SwitchMode args failed: %w", err) } if err := validateSwitchModeTargetID(args.GetTargetModeId()); err != nil { return nil, runtimecore.PendingInteraction{}, err } args.ToolCallId = toolCall.CallID messageID := bridge.nextMessageID() serverMessage := &agentv1.AgentServerMessage{ Message: &agentv1.AgentServerMessage_InteractionQuery{ InteractionQuery: &agentv1.InteractionQuery{ Id: messageID, Query: &agentv1.InteractionQuery_SwitchModeRequestQuery{ SwitchModeRequestQuery: &agentv1.SwitchModeRequestQuery{ Args: &args, }, }, }, }, } argsPayload, _ := json.Marshal(args) return serverMessage, runtimecore.PendingInteraction{ InteractionID: fmt.Sprintf("%d", messageID), ArgsJSON: argsPayload, ToolCallID: toolCall.CallID, InteractionKind: "switch_mode", }, nil } func validateSwitchModeTargetID(raw string) error { switch strings.ToLower(strings.TrimSpace(raw)) { case "agent", "ask", "plan": return nil default: return fmt.Errorf("unsupported target mode id: %q", strings.TrimSpace(raw)) } } // summarizeAskQuestionResponse 生成 AskQuestion 响应摘要。 func summarizeAskQuestionResponse(response *agentv1.AskQuestionInteractionResponse) string { if response == nil || response.GetResult() == nil { return "ask question response missing" } switch item := response.GetResult().GetResult().(type) { case *agentv1.AskQuestionResult_Success: if len(item.Success.GetAnswers()) == 0 { return "ask question success" } return fmt.Sprintf("ask question answers=%d", len(item.Success.GetAnswers())) case *agentv1.AskQuestionResult_Error: return item.Error.GetErrorMessage() case *agentv1.AskQuestionResult_Rejected: return item.Rejected.GetReason() case *agentv1.AskQuestionResult_Async: return "ask question async accepted" default: return "unknown ask question response" } } const createPlanEmptyURIError = "create plan failed: Cursor returned success with empty planUri" // normalizeCreatePlanResult 兜底客户端 success 但未返回 planUri 的异常形态。 func normalizeCreatePlanResult(response *agentv1.CreatePlanRequestResponse) *agentv1.CreatePlanResult { if response == nil || response.GetResult() == nil { return nil } result := response.GetResult() if result.GetSuccess() != nil && strings.TrimSpace(result.GetPlanUri()) == "" { return &agentv1.CreatePlanResult{ Result: &agentv1.CreatePlanResult_Error{ Error: &agentv1.CreatePlanError{Error: createPlanEmptyURIError}, }, } } return result } // summarizeCreatePlanResult 生成 CreatePlan 响应摘要。 func summarizeCreatePlanResult(result *agentv1.CreatePlanResult) string { if result == nil { return "create plan response missing" } switch item := result.GetResult().(type) { case *agentv1.CreatePlanResult_Success: return fmt.Sprintf("create plan success uri=%s", result.GetPlanUri()) case *agentv1.CreatePlanResult_Error: return item.Error.GetError() default: return "unknown create plan response" } } // summarizeWebSearchResponse 生成 WebSearch 响应摘要。 func summarizeWebSearchResponse(response *agentv1.WebSearchRequestResponse) string { if response == nil { return "web search response missing" } switch item := response.GetResult().(type) { case *agentv1.WebSearchRequestResponse_Approved_: _ = item return "web search approved" case *agentv1.WebSearchRequestResponse_Rejected_: return item.Rejected.GetReason() default: return "unknown web search response" } } // applyWebSearchResponse 把 WebSearch approval 响应转换成最终工具结果。 func (bridge *Bridge) applyWebSearchResponse(response *agentv1.WebSearchRequestResponse, args *agentv1.WebSearchArgs) (*agentv1.WebSearchResult, string) { if response == nil { return &agentv1.WebSearchResult{ Result: &agentv1.WebSearchResult_Error{ Error: &agentv1.WebSearchError{Error: "web search response missing"}, }, }, "web search response missing" } switch item := response.GetResult().(type) { case *agentv1.WebSearchRequestResponse_Approved_: _ = item references, payload, err := bridge.executeWebSearch(strings.TrimSpace(args.GetSearchTerm())) if err != nil { return &agentv1.WebSearchResult{ Result: &agentv1.WebSearchResult_Error{ Error: &agentv1.WebSearchError{Error: err.Error()}, }, }, err.Error() } references, payload = truncateWebSearchReplay(strings.TrimSpace(args.GetSearchTerm()), references, payload) return &agentv1.WebSearchResult{ Result: &agentv1.WebSearchResult_Success{ Success: &agentv1.WebSearchSuccess{References: references}, }, }, payload case *agentv1.WebSearchRequestResponse_Rejected_: return &agentv1.WebSearchResult{ Result: &agentv1.WebSearchResult_Rejected{ Rejected: &agentv1.WebSearchRejected{Reason: item.Rejected.GetReason()}, }, }, item.Rejected.GetReason() default: return &agentv1.WebSearchResult{ Result: &agentv1.WebSearchResult_Error{ Error: &agentv1.WebSearchError{Error: "unknown web search response"}, }, }, "unknown web search response" } } // applyWebFetchResponse 把 WebFetch approval 响应转换成最终工具结果。 func (bridge *Bridge) applyWebFetchResponse(response *agentv1.WebFetchRequestResponse, args *agentv1.WebFetchArgs) (*agentv1.WebFetchResult, string) { if response == nil { return &agentv1.WebFetchResult{ Result: &agentv1.WebFetchResult_Error{ Error: &agentv1.WebFetchError{ Url: args.GetUrl(), Error: "web fetch response missing", }, }, }, "web fetch response missing" } switch item := response.GetResult().(type) { case *agentv1.WebFetchRequestResponse_Approved_: _ = item markdown, err := bridge.executeWebFetch(strings.TrimSpace(args.GetUrl())) if err != nil { return &agentv1.WebFetchResult{ Result: &agentv1.WebFetchResult_Error{ Error: &agentv1.WebFetchError{ Url: args.GetUrl(), Error: err.Error(), }, }, }, err.Error() } return &agentv1.WebFetchResult{ Result: &agentv1.WebFetchResult_Success{ Success: &agentv1.WebFetchSuccess{ Url: args.GetUrl(), Markdown: markdown, }, }, }, markdown case *agentv1.WebFetchRequestResponse_Rejected_: return &agentv1.WebFetchResult{ Result: &agentv1.WebFetchResult_Rejected{ Rejected: &agentv1.WebFetchRejected{Reason: item.Rejected.GetReason()}, }, }, item.Rejected.GetReason() default: return &agentv1.WebFetchResult{ Result: &agentv1.WebFetchResult_Error{ Error: &agentv1.WebFetchError{ Url: args.GetUrl(), Error: "unknown web fetch response", }, }, }, "unknown web fetch response" } } // buildSwitchModeResult 把 SwitchMode approval 响应转换成最终工具结果。 func buildSwitchModeResult(response *agentv1.SwitchModeRequestResponse, args *agentv1.SwitchModeArgs) *agentv1.SwitchModeResult { if response == nil { return &agentv1.SwitchModeResult{ Result: &agentv1.SwitchModeResult_Error{ Error: &agentv1.SwitchModeError{Error: "switch mode response missing"}, }, } } switch item := response.GetResult().(type) { case *agentv1.SwitchModeRequestResponse_Approved_: _ = item targetModeID := strings.ToLower(strings.TrimSpace(args.GetTargetModeId())) return &agentv1.SwitchModeResult{ Result: &agentv1.SwitchModeResult_Success{ Success: &agentv1.SwitchModeSuccess{ FromModeId: "unknown", ToModeId: targetModeID, }, }, } case *agentv1.SwitchModeRequestResponse_Rejected_: return &agentv1.SwitchModeResult{ Result: &agentv1.SwitchModeResult_Rejected{ Rejected: &agentv1.SwitchModeRejected{Reason: item.Rejected.GetReason()}, }, } default: return &agentv1.SwitchModeResult{ Result: &agentv1.SwitchModeResult_Error{ Error: &agentv1.SwitchModeError{Error: "unknown switch mode response"}, }, } } } // summarizeSwitchModeResponse 生成 SwitchMode 响应摘要。 func summarizeSwitchModeResponse(result *agentv1.SwitchModeResult) string { if result == nil { return "switch mode result missing" } switch item := result.GetResult().(type) { case *agentv1.SwitchModeResult_Success: return fmt.Sprintf("switch mode success to=%s", item.Success.GetToModeId()) case *agentv1.SwitchModeResult_Rejected: return item.Rejected.GetReason() case *agentv1.SwitchModeResult_Error: return item.Error.GetError() default: return "unknown switch mode result" } } var ( webSearchAnchorPattern = regexp.MustCompile(`(?is)]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>(.*?)`) webSearchSnippetPattern = regexp.MustCompile(`(?is)<(?:a|div)[^>]*class="[^"]*result__snippet[^"]*"[^>]*>(.*?)(?:a|div)>`) htmlTitlePattern = regexp.MustCompile(`(?is)