From 9add5df1b24d633fd3d5ab306c4e113352dc18be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Thu, 24 Jul 2025 17:06:10 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=89=AA=E8=B4=B4=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uixt/android_driver_adb.go | 54 ++++++++++++++++++++++++++++++++++++++ uixt/android_test.go | 7 +++++ uixt/ios_driver_wda.go | 8 ++++-- uixt/ios_test.go | 7 +++++ uixt/option/android.go | 5 ++-- 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 1e539364..0f54b9c3 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "encoding/base64" "encoding/json" "encoding/xml" "fmt" @@ -1166,6 +1167,59 @@ func (ad *ADBDriver) ClearFiles(paths ...string) error { return nil } +func (ad *ADBDriver) GetPasteboard() (content string, err error) { + /** + adb shell am broadcast -n io.appium.settings/.receivers.ClipboardReceiver -a io.appium.settings.clipboard.get + Broadcasting: Intent { act=io.appium.settings.clipboard.get flg=0x400000 cmp=io.appium.settings/.receivers.ClipboardReceiver } + Broadcast completed: result=-1, data="SEhHRw==" + **/ + + // Check and switch input method if needed, similar to SendUnicodeKeys + currentIme, err := ad.GetIme() + if err != nil { + return "", err + } + + // If current IME is not the required one, switch temporarily and restore later + if currentIme != option.UnicodeImePackageName { + defer func() { + _ = ad.SetIme(currentIme) + }() + + err = ad.SetIme(option.UnicodeImePackageName) + if err != nil { + log.Warn().Err(err).Msgf("set Unicode Ime failed for clipboard operation") + // Continue anyway, might still work with current IME + } + } + + res, err := ad.Device.RunShellCommand("am", "broadcast", "-n", option.AppiumSettingsPackageName+"/.receivers.ClipboardReceiver", "-a", option.AppiumSettingsPackageName+".clipboard.get") + if err != nil { + return "", err + } + + // Parse the response to extract the base64 encoded data + dataPrefix := "data=\"" + dataIndex := strings.Index(res, dataPrefix) + if dataIndex == -1 { + return "", fmt.Errorf("clipboard data not found in response: %s", res) + } + + dataStart := dataIndex + len(dataPrefix) + dataEnd := strings.Index(res[dataStart:], "\"") + if dataEnd == -1 { + return "", fmt.Errorf("malformed clipboard data in response: %s", res) + } + + base64Data := res[dataStart : dataStart+dataEnd] + decodedData, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + return "", fmt.Errorf("failed to decode clipboard content: %w", err) + } + + return string(decodedData), nil +} + type ExportPoint struct { Start int `json:"start" yaml:"start"` End int `json:"end" yaml:"end"` diff --git a/uixt/android_test.go b/uixt/android_test.go index 794b9160..b8a085be 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -321,3 +321,10 @@ func TestDriver_UIA2_Input(t *testing.T) { err = driver.Input("123\n") assert.Nil(t, err) } + +func TestDriver_ADB_GetPasteboard(t *testing.T) { + driver := setupADBDriverExt(t) + pasteboard, err := driver.IDriver.(*ADBDriver).GetPasteboard() + assert.Nil(t, err) + t.Log(pasteboard) +} diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index b7d68474..02be5bac 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -630,9 +630,13 @@ func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content str return } -func (wd *WDADriver) GetPasteboard(contentType types.PasteboardType) (raw *bytes.Buffer, err error) { +func (wd *WDADriver) GetPasteboard() (raw *bytes.Buffer, err error) { // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] - data := map[string]interface{}{"contentType": contentType} + err = wd.AppLaunch("com.gtf.wda.runner.xctrunner") + if err != nil { + return nil, errors.Wrap(err, "GetPasteboard failed. WDA app not launched") + } + data := map[string]interface{}{} var rawResp DriverRawResponse if rawResp, err = wd.Session.POST(data, "/wda/getPasteboard"); err != nil { return nil, err diff --git a/uixt/ios_test.go b/uixt/ios_test.go index 0a0bbde0..6ec08066 100644 --- a/uixt/ios_test.go +++ b/uixt/ios_test.go @@ -356,3 +356,10 @@ func TestDriver_WDA_PushImage(t *testing.T) { err = driver.ClearImages() assert.Nil(t, err) } + +func TestDriver_WDA_GetPasteboard(t *testing.T) { + driver := setupWDADriverExt(t) + pasteboard, err := driver.IDriver.(*WDADriver).GetPasteboard() + assert.Nil(t, err) + t.Log(pasteboard) +} diff --git a/uixt/option/android.go b/uixt/option/android.go index bfa991a6..dc3d4e35 100644 --- a/uixt/option/android.go +++ b/uixt/option/android.go @@ -49,8 +49,9 @@ const ( defaultUIA2ServerPackageName = "io.appium.uiautomator2.server" defaultUIA2ServerTestPackageName = "io.appium.uiautomator2.server.test" - AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME" - UnicodeImePackageName = "io.appium.settings/.UnicodeIME" + AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME" + UnicodeImePackageName = AppiumSettingsPackageName + "/.UnicodeIME" + AppiumSettingsPackageName = "io.appium.settings" ) func NewAndroidDeviceOptions(opts ...AndroidDeviceOption) *AndroidDeviceOptions { From 8b20fceb63f10ea27808f3d33a437fdc3d192978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Thu, 24 Jul 2025 17:26:25 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=89=AA=E8=B4=B4=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uixt/android_driver_uia2.go | 16 ++++------- uixt/browser_driver.go | 5 +++- uixt/driver.go | 3 ++ uixt/harmony_driver_hdc.go | 4 +++ uixt/ios_driver_wda.go | 11 +++---- uixt/mcp_tools_pasteboard.go | 56 ++++++++++++++++++++++++++++++++++++ uixt/option/action.go | 3 +- 7 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 uixt/mcp_tools_pasteboard.go diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 70390860..7e7776e1 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -574,30 +574,26 @@ func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content st return } -func (ud *UIA2Driver) GetPasteboard(contentType types.PasteboardType) (raw *bytes.Buffer, err error) { - if len(contentType) == 0 { - contentType = types.PasteboardTypePlaintext - } +func (ud *UIA2Driver) GetPasteboard() (content string, err error) { // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) data := map[string]interface{}{ - "contentType": contentType[0], + "contentType": types.PasteboardTypePlaintext, } var rawResp DriverRawResponse urlStr := fmt.Sprintf("/session/%s/appium/device/get_clipboard", ud.Session.ID) if rawResp, err = ud.Session.POST(data, urlStr); err != nil { - return + return "", err } reply := new(struct{ Value string }) if err = json.Unmarshal(rawResp, reply); err != nil { - return + return "", err } if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil { - raw.Write([]byte(reply.Value)) + return reply.Value, nil } else { - raw.Write(data) + return string(data), nil } - return } // SendKeys Android input does not support setting frequency. diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 28820a81..6a98bcf5 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -365,7 +365,6 @@ func (wd *BrowserDriver) Input(text string, option ...option.ActionOption) (err // Source Return application elements tree func (wd *BrowserDriver) Source(srcOpt ...option.SourceOption) (string, error) { result, err := wd.CustomeGet(wd.concatURL(wd.Session.ID, "stub/source")) - if err != nil { return "", err } @@ -751,3 +750,7 @@ func (wd *BrowserDriver) CustomeGet(urlStr string) (response *WebAgentResponse, return &webResp, err } + +func (wd *BrowserDriver) GetPasteboard() (content string, err error) { + return "", errors.New("not implemented") +} diff --git a/uixt/driver.go b/uixt/driver.go index 718ed821..58d31fc6 100644 --- a/uixt/driver.go +++ b/uixt/driver.go @@ -86,4 +86,7 @@ type IDriver interface { // triggers the log capture and returns the log entries StartCaptureLog(identifier ...string) error StopCaptureLog() (result interface{}, err error) + + // clipboard operations + GetPasteboard() (string, error) } diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index c4c93e92..398251cb 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -348,3 +348,7 @@ func (hd *HDCDriver) SecondaryClick(x, y float64) (err error) { func (hd *HDCDriver) SecondaryClickBySelector(selector string, options ...option.ActionOption) (err error) { return err } + +func (hd *HDCDriver) GetPasteboard() (content string, err error) { + return "", errors.New("not implemented") +} diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 02be5bac..9042c736 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -630,21 +630,22 @@ func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content str return } -func (wd *WDADriver) GetPasteboard() (raw *bytes.Buffer, err error) { +func (wd *WDADriver) GetPasteboard() (content string, err error) { // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] err = wd.AppLaunch("com.gtf.wda.runner.xctrunner") if err != nil { - return nil, errors.Wrap(err, "GetPasteboard failed. WDA app not launched") + return "", errors.Wrap(err, "GetPasteboard failed. WDA app not launched") } data := map[string]interface{}{} var rawResp DriverRawResponse if rawResp, err = wd.Session.POST(data, "/wda/getPasteboard"); err != nil { - return nil, err + return "", err } + var raw *bytes.Buffer if raw, err = rawResp.ValueDecodeAsBase64(); err != nil { - return nil, err + return "", err } - return + return string(raw.Bytes()), nil } func (wd *WDADriver) SetIme(ime string) error { diff --git a/uixt/mcp_tools_pasteboard.go b/uixt/mcp_tools_pasteboard.go new file mode 100644 index 00000000..4fb1c406 --- /dev/null +++ b/uixt/mcp_tools_pasteboard.go @@ -0,0 +1,56 @@ +package uixt + +import ( + "context" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + + "github.com/httprunner/httprunner/v5/uixt/option" +) + +// ToolGetPasteboard implements the get_pasteboard tool call. +type ToolGetPasteboard struct { + // Return data fields - these define the structure of data returned by this tool + Content string `json:"content" desc:"Clipboard content that was retrieved"` +} + +func (t *ToolGetPasteboard) Name() option.ActionName { + return option.ACTION_GetPasteboard +} + +func (t *ToolGetPasteboard) Description() string { + return "Get the clipboard content from the device" +} + +func (t *ToolGetPasteboard) Options() []mcp.ToolOption { + unifiedReq := &option.ActionOptions{} + return unifiedReq.GetMCPOptions(option.ACTION_GetPasteboard) +} + +func (t *ToolGetPasteboard) Implement() server.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.GetArguments() + driverExt, err := setupXTDriver(ctx, arguments) + if err != nil { + return nil, fmt.Errorf("setup driver failed: %w", err) + } + + // Directly call the GetPasteboard method on the driver + content, err := driverExt.IDriver.GetPasteboard() + if err != nil { + return NewMCPErrorResponse(fmt.Sprintf("Get pasteboard failed: %s", err.Error())), err + } + + message := "Successfully retrieved clipboard content" + returnData := ToolGetPasteboard{Content: content} + + return NewMCPSuccessResponse(message, &returnData), nil + } +} + +func (t *ToolGetPasteboard) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { + arguments := map[string]any{} + return BuildMCPCallToolRequest(t.Name(), arguments, action), nil +} diff --git a/uixt/option/action.go b/uixt/option/action.go index edbffc31..a6f40736 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -55,7 +55,8 @@ const ( ACTION_SetIme ActionName = "set_ime" ACTION_GetSource ActionName = "get_source" ACTION_GetForegroundApp ActionName = "get_foreground_app" - ACTION_AppInfo ActionName = "app_info" // get app info action + ACTION_GetPasteboard ActionName = "get_pasteboard" // get clipboard content + ACTION_AppInfo ActionName = "app_info" // get app info action // UI handling ACTION_Home ActionName = "home" From ac77975c1884d0674dd9710b685f7dc34da821c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Thu, 24 Jul 2025 17:34:02 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4ui2=20=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=89=AA=E8=B4=B4=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uixt/android_driver_uia2.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 7e7776e1..6514f09b 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -574,28 +574,6 @@ func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content st return } -func (ud *UIA2Driver) GetPasteboard() (content string, err error) { - // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) - data := map[string]interface{}{ - "contentType": types.PasteboardTypePlaintext, - } - var rawResp DriverRawResponse - urlStr := fmt.Sprintf("/session/%s/appium/device/get_clipboard", ud.Session.ID) - if rawResp, err = ud.Session.POST(data, urlStr); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil { - return reply.Value, nil - } else { - return string(data), nil - } -} - // SendKeys Android input does not support setting frequency. func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (err error) { log.Info().Str("text", text).Msg("UIA2Driver.Input")