From dda9dbe915e271a91e114e4530f7548d1044b12e Mon Sep 17 00:00:00 2001 From: Josh at WLTechBlog Date: Mon, 18 Aug 2025 13:30:18 -0500 Subject: [PATCH] multiple --- client/client.go | 10 +++-- daemon/daemon.go | 111 +++++++++++++++++++++++++++++++++++++---------- mcp/cremote-mcp | Bin 10026987 -> 10026987 bytes 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/client/client.go b/client/client.go index 1ffe019..20a80ba 100644 --- a/client/client.go +++ b/client/client.go @@ -14,7 +14,8 @@ import ( // Client is the client for communicating with the daemon type Client struct { - serverURL string + serverURL string + httpClient *http.Client } // Command represents a command sent from the client to the daemon @@ -34,12 +35,15 @@ type Response struct { func NewClient(host string, port int) *Client { return &Client{ serverURL: fmt.Sprintf("http://%s:%d", host, port), + httpClient: &http.Client{ + Timeout: 60 * time.Second, // 60 second timeout for long operations + }, } } // CheckStatus checks if the daemon is running func (c *Client) CheckStatus() (bool, error) { - resp, err := http.Get(c.serverURL + "/status") + resp, err := c.httpClient.Get(c.serverURL + "/status") if err != nil { return false, err } @@ -174,7 +178,7 @@ func (c *Client) SendCommand(action string, params map[string]string) (*Response return nil, err } - resp, err := http.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData)) + resp, err := c.httpClient.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData)) if err != nil { return nil, err } diff --git a/daemon/daemon.go b/daemon/daemon.go index f005f94..0fe847a 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -2822,21 +2822,40 @@ func (d *Daemon) extractMultiple(tabID, selectorsJSON string, timeout int) (*Mul continue } - // Extract text content from all matching elements - var texts []string + // Extract content from all matching elements + var values []string for _, element := range elements { - text, err := element.Text() + var value string + var err error + + // Check if it's a form input element and get its value + tagName, _ := element.Eval("() => this.tagName.toLowerCase()") + + if tagName.Value.Str() == "input" || tagName.Value.Str() == "textarea" || tagName.Value.Str() == "select" { + // For form elements, get the value property + valueProp, err := element.Property("value") + if err == nil && valueProp.Str() != "" { + value = valueProp.Str() + } else { + // Fallback to text content + value, err = element.Text() + } + } else { + // For non-form elements, get text content + value, err = element.Text() + } + if err != nil { - result.Errors[key] = fmt.Sprintf("failed to get text: %v", err) + result.Errors[key] = fmt.Sprintf("failed to get content: %v", err) break } - texts = append(texts, text) + values = append(values, value) } - if len(texts) == 1 { - result.Results[key] = texts[0] + if len(values) == 1 { + result.Results[key] = values[0] } else { - result.Results[key] = texts + result.Results[key] = values } } @@ -3414,15 +3433,9 @@ func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) ( Success: false, } - // Find the element + // Find the element without timeout to avoid context cancellation issues var element *rod.Element - if timeout > 0 { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - element, err = page.Context(ctx).Element(interaction.Selector) - cancel() - } else { - element, err = page.Element(interaction.Selector) - } + element, err = page.Element(interaction.Selector) if err != nil { interactionResult.Error = fmt.Sprintf("failed to find element: %v", err) @@ -3435,6 +3448,14 @@ func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) ( switch interaction.Action { case "click": err = element.Click(proto.InputMouseButtonLeft, 1) + // Retry once if context was canceled + if err != nil && strings.Contains(err.Error(), "context canceled") { + // Try to find element again and click + element, err = page.Element(interaction.Selector) + if err == nil { + err = element.Click(proto.InputMouseButtonLeft, 1) + } + } if err != nil { interactionResult.Error = fmt.Sprintf("failed to click: %v", err) } else { @@ -3450,6 +3471,20 @@ func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) ( if err == nil { err = element.Input(interaction.Value) } + // Retry once if context was canceled + if err != nil && strings.Contains(err.Error(), "context canceled") { + // Try to find element again and fill + element, err = page.Element(interaction.Selector) + if err == nil { + err = element.SelectAllText() + if err == nil { + err = element.Input("") + } + if err == nil { + err = element.Input(interaction.Value) + } + } + } if err != nil { interactionResult.Error = fmt.Sprintf("failed to fill: %v", err) } else { @@ -3457,14 +3492,34 @@ func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) ( } case "select": - // For select elements, try to select by text first, then by value - err = element.Select([]string{interaction.Value}, true, rod.SelectorTypeText) - if err != nil { - // Try by value if text selection failed - err = element.Select([]string{interaction.Value}, false, rod.SelectorTypeText) - } - if err != nil { - interactionResult.Error = fmt.Sprintf("failed to select: %v", err) + // For select elements, use JavaScript to set the value + script := fmt.Sprintf(` + const element = arguments[0]; + if (element.tagName.toLowerCase() === 'select') { + // Try to select by value first + for (let option of element.options) { + if (option.value === '%s') { + element.value = '%s'; + element.dispatchEvent(new Event('change', { bubbles: true })); + return true; + } + } + // Try to select by text if value didn't work + for (let option of element.options) { + if (option.text === '%s') { + element.value = option.value; + element.dispatchEvent(new Event('change', { bubbles: true })); + return true; + } + } + return false; + } + return false; + `, interaction.Value, interaction.Value, interaction.Value) + + result, err := element.Eval(script) + if err != nil || !result.Value.Bool() { + interactionResult.Error = fmt.Sprintf("failed to select option: %s", interaction.Value) } else { interactionResult.Success = true } @@ -3476,6 +3531,14 @@ func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) ( interactionResult.Success = true // Already checked } else { err = element.Click(proto.InputMouseButtonLeft, 1) + // Retry once if context was canceled + if err != nil && strings.Contains(err.Error(), "context canceled") { + // Try to find element again and click + element, err = page.Element(interaction.Selector) + if err == nil { + err = element.Click(proto.InputMouseButtonLeft, 1) + } + } if err != nil { interactionResult.Error = fmt.Sprintf("failed to check: %v", err) } else { diff --git a/mcp/cremote-mcp b/mcp/cremote-mcp index 0d7457032cdd685043c852b2b4648469f3bd82a2..935c2c907eb1a8bc892c70845e13ace97a7b967c 100755 GIT binary patch delta 927 zcmb`?Nmo(<0ES^o$|j@CEX(G}h8y=@#!FFo!vF^~^n%eqml;JX5e*u&tZ2?g`U|zN zP8(MJ2hXuZ`)b9eWxIOY^#fWwo9~=&@z#ESGuH4iXPVN(V%ir>%AF(5OvDi#5Bf@Z zbD$WG#nj2PM+k@F{enEv9xRy0LOqdWNpueRRjEsotpnb{usM(qj>_5MaJRpsub>r$ zXfD-jFd7VoCe*Ib*yow4oTNmpVCQ5UUtUAHEjwq#s!#C3a0NhonkWm@&L);nFIF!T*75s!5H!wM*$O<#8nhg!ZlpS4cx>Org00mF@rm}i+d>J hJ|5s99^o;b;3=MA7SHhlFYyYm@dlOZoc7lJ{tpgddRG7d delta 927 zcmb`?OIH#B0L5`q%3em2S(cT(?Lo{8!#Gg%7)OW*$Pf{FA~1##Bq=F9w5;g;wCE#L z3+r@Fhtqdx;j(>8K0xQRseijZK#Skzo_iO!{`-fyj!!wuRHRTA;$g|34veWGqoDgE z<5_!ID|jPnUoa+4mU?V4uMly@?Zts$B2yOB5x*t|L`m-V4aV&Ge4vZ(gvI)E_E(ENmmN}M`%K+ZsaqDH0gW(Y zAr_$ti?IYtu?)-6j1~C1U5OT0u?nqNjW(>oTC`&w)}sR(uo0WkiOtx8t=NX`*nyqs z!Y=H_9_)n;`(TFv5fU7b;e>*2sBpoJ{Wt&*4&o3F;|MhL)an-fv(2gqf~eT!Owy@{ z&WxcdO6{XZe{b&kH%cZARd7fSha}1=RZ=B6P1%)BIbGe=X-)5FQrv>z7M1E;G-a#I zYdU)2g%3yJ$1wzO96^K-#tHP{BqHcX6a$E15OE}M3a4=fXK@bak;G7KUQ2y%{?D%} zyh0io7#PL{WHEw^$l((5DBvOrg00mF@rm}i+iZx hJ|5s99^o;b;3=NrIbPr;USSrm@dnk}T=cEy!yh1fcenrm