Skip to content

Commit 479087c

Browse files
authored
Merge pull request #1175 from hchen2020/master
Add IWebElementLocator
2 parents cd604bf + c852ac4 commit 479087c

File tree

15 files changed

+315
-32
lines changed

15 files changed

+315
-32
lines changed

src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace BotSharp.Abstraction.Browsing.Models;
55
[DebuggerStepThrough]
66
public class ElementLocatingArgs
77
{
8+
[JsonPropertyName("element_locator_desc")]
9+
public string ElementLocatorDescription { get; set; } = string.Empty;
10+
811
[JsonPropertyName("match_rule")]
912
public string MatchRule { get; set; } = string.Empty;
1013

@@ -26,6 +29,8 @@ public class ElementLocatingArgs
2629
[JsonPropertyName("selector")]
2730
public string? Selector { get; set; }
2831

32+
public ElementPosition? Position { get; set; }
33+
2934
public bool Parent { get; set; }
3035

3136
public bool FailIfMultiple { get; set; }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using BotSharp.Abstraction.Browsing.Models;
2+
3+
namespace BotSharp.Abstraction.Browsing.Settings;
4+
5+
public interface IWebElementLocator
6+
{
7+
Task<ElementPosition> DetectElementCoordinates(IWebBrowser browser, string contextId, string elementDescription);
8+
}

src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-web-action_on_element.fn.liquid" />
20-
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-web-go_to_page.fn.liquid" />
2119
<None Remove="data\agents\f3ae2a0f-e6ba-4ee1-a0b9-75d7431ff32b\instructions\instruction.liquid" />
2220
<None Remove="data\agents\f3ae2a0f-e6ba-4ee1-a0b9-75d7431ff32b\templates\extract_data.liquid" />
2321
<None Remove="data\agents\f3ae2a0f-e6ba-4ee1-a0b9-75d7431ff32b\templates\html_parser.liquid" />
@@ -28,6 +26,9 @@
2826
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-web-action_on_element.json">
2927
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3028
</Content>
29+
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-web-take_screenshot.json">
30+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
31+
</Content>
3132
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-web-close_browser.json">
3233
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3334
</Content>
@@ -37,12 +38,6 @@
3738
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-web-locate_element.json">
3839
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3940
</Content>
40-
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-web-action_on_element.fn.liquid">
41-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42-
</Content>
43-
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-web-go_to_page.fn.liquid">
44-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
45-
</Content>
4641
<Content Include="data\agents\f3ae2a0f-e6ba-4ee1-a0b9-75d7431ff32b\agent.json">
4742
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4843
</Content>

src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ public async Task<BrowserActionResult> ActionOnElement(MessageInfo message, Elem
88
var result = await LocateElement(message, location);
99
if (result.IsSuccess)
1010
{
11+
action.Position = location.Position;
1112
await DoAction(message, action, result);
12-
result.UrlAfterAction = _instance.GetPage(message.ContextId)?.Url;
1313
}
14+
15+
result.UrlAfterAction = _instance.GetPage(message.ContextId)?.Url;
16+
1417
return result;
1518
}
1619
}

src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs

Lines changed: 211 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO;
22
using System.Net.Http;
3+
using System.Xml.Linq;
34

45
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
56

@@ -8,33 +9,66 @@ public partial class PlaywrightWebDriver
89
public async Task DoAction(MessageInfo message, ElementActionArgs action, BrowserActionResult result)
910
{
1011
var page = _instance.GetPage(message.ContextId);
11-
if (string.IsNullOrEmpty(result.Selector))
12+
if (string.IsNullOrEmpty(result.Selector) && action.Position == null)
1213
{
1314
Serilog.Log.Error($"Selector is not set.");
1415
return;
1516
}
1617

17-
ILocator locator = page.Locator(result.Selector);
18-
var count = await locator.CountAsync();
19-
20-
if (count == 0)
21-
{
22-
Serilog.Log.Error($"Element not found: {result.Selector}");
23-
return;
24-
}
25-
else if (count > 1)
18+
ILocator? locator;
19+
20+
if (result.Selector != null)
2621
{
27-
if (!action.FirstIfMultipleFound)
22+
locator = page.Locator(result.Selector);
23+
24+
var count = await locator.CountAsync();
25+
26+
if (count == 0)
2827
{
29-
Serilog.Log.Error($"Multiple eElements were found: {result.Selector}");
28+
Serilog.Log.Error($"Element not found: {result.Selector}");
3029
return;
3130
}
32-
else
31+
else if (count > 1)
3332
{
34-
locator = page.Locator(result.Selector).First;// 匹配到多个时取第一个,否则当await locator.ClickAsync();匹配到多个就会抛异常。
33+
if (!action.FirstIfMultipleFound)
34+
{
35+
Serilog.Log.Error($"Multiple eElements were found: {result.Selector}");
36+
return;
37+
}
38+
else
39+
{
40+
locator = page.Locator(result.Selector).First;// 匹配到多个时取第一个,否则当await locator.ClickAsync();匹配到多个就会抛异常。
41+
}
3542
}
43+
44+
await ExecuteAction(message, page, locator, action);
3645
}
46+
else if (action.Position != null && action.Position.X != 0 && action.Position.Y != 0)
47+
{
48+
if (action.Position != null && action.Position.X != 0 && action.Position.Y != 0)
49+
{
50+
var elementHandle = await page.EvaluateHandleAsync(
51+
@"(coords) => document.elementFromPoint(coords.x, coords.y)",
52+
new { x = (int)action.Position.X, y = (int)action.Position.Y }
53+
);
3754

55+
await ExecuteAction(message, page, elementHandle.AsElement(), action);
56+
}
57+
}
58+
else
59+
{
60+
Serilog.Log.Error($"Selector or position is not set.");
61+
return;
62+
}
63+
64+
if (action.WaitTime > 0)
65+
{
66+
await Task.Delay(1000 * action.WaitTime);
67+
}
68+
}
69+
70+
private async Task ExecuteAction(MessageInfo message, IPage page, ILocator locator, ElementActionArgs action)
71+
{
3872
if (action.Action == BroswerActionEnum.Click)
3973
{
4074
if (action.Position == null)
@@ -201,12 +235,174 @@ await locator.ClickAsync(new LocatorClickOptions
201235
}
202236
}
203237
}
238+
}
204239

205-
if (action.WaitTime > 0)
240+
private async Task ExecuteAction(MessageInfo message, IPage page, IElementHandle elementHandle, ElementActionArgs action)
241+
{
242+
var body = page.Locator("body");
243+
244+
if (action.Action == BroswerActionEnum.Click)
206245
{
246+
await body.ClickAsync(new LocatorClickOptions
247+
{
248+
Position = new Position
249+
{
250+
X = action.Position.X,
251+
Y = action.Position.Y
252+
}
253+
});
254+
}
255+
else if (action.Action == BroswerActionEnum.DropDown)
256+
{
257+
var tagName = await body.EvaluateAsync<string>("el => el.tagName.toLowerCase()");
258+
if (tagName == "select")
259+
{
260+
await HandleSelectDropDownAsync(page, body, action);
261+
}
262+
else
263+
{
264+
await body.ClickAsync();
265+
if (!string.IsNullOrWhiteSpace(action.PressKey))
266+
{
267+
await page.Keyboard.PressAsync(action.PressKey);
268+
await page.Keyboard.PressAsync("Enter");
269+
}
270+
else
271+
{
272+
var optionLocator = page.Locator($"//div[text()='{action.Content}']");
273+
var optionCount = await optionLocator.CountAsync();
274+
if (optionCount == 0)
275+
{
276+
Serilog.Log.Error($"Dropdown option not found: {action.Content}");
277+
return;
278+
}
279+
await optionLocator.First.ClickAsync();
280+
}
281+
}
282+
}
283+
else if (action.Action == BroswerActionEnum.InputText)
284+
{
285+
await elementHandle.FillAsync(action.Content);
286+
287+
if (action.PressKey != null)
288+
{
289+
if (action.DelayBeforePressingKey > 0)
290+
{
291+
await Task.Delay(action.DelayBeforePressingKey);
292+
}
293+
await body.PressAsync(action.PressKey);
294+
}
295+
}
296+
else if (action.Action == BroswerActionEnum.FileUpload)
297+
{
298+
var _states = _services.GetRequiredService<IConversationStateService>();
299+
var files = new List<string>();
300+
if (action.FileUrl != null && action.FileUrl.Length > 0)
301+
{
302+
files.AddRange(action.FileUrl);
303+
}
304+
var hooks = _services.GetServices<IWebDriverHook>();
305+
foreach (var hook in hooks)
306+
{
307+
files.AddRange(await hook.GetUploadFiles(message));
308+
}
309+
if (files.Count == 0)
310+
{
311+
Serilog.Log.Warning($"No files found to upload: {action.Content}");
312+
return;
313+
}
314+
var fileChooser = await page.RunAndWaitForFileChooserAsync(async () =>
315+
{
316+
await body.ClickAsync();
317+
});
318+
var guid = Guid.NewGuid().ToString();
319+
var directory = Path.Combine(Path.GetTempPath(), guid);
320+
DeleteDirectory(directory);
321+
Directory.CreateDirectory(directory);
322+
var localPaths = new List<string>();
323+
var http = _services.GetRequiredService<IHttpClientFactory>();
324+
using var httpClient = http.CreateClient();
325+
foreach (var fileUrl in files)
326+
{
327+
try
328+
{
329+
using var fileData = await httpClient.GetAsync(fileUrl);
330+
var fileName = new Uri(fileUrl).AbsolutePath;
331+
var localPath = Path.Combine(directory, Path.GetFileName(fileName));
332+
await using var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
333+
await fileData.Content.CopyToAsync(fs);
334+
localPaths.Add(localPath);
335+
}
336+
catch (Exception ex)
337+
{
338+
Serilog.Log.Error($"FileUpload failed for {fileUrl}. Message: {ex.Message}");
339+
}
340+
}
341+
await fileChooser.SetFilesAsync(localPaths);
207342
await Task.Delay(1000 * action.WaitTime);
208343
}
344+
else if (action.Action == BroswerActionEnum.Typing)
345+
{
346+
await body.PressSequentiallyAsync(action.Content);
347+
if (action.PressKey != null)
348+
{
349+
if (action.DelayBeforePressingKey > 0)
350+
{
351+
await Task.Delay(action.DelayBeforePressingKey);
352+
}
353+
await body.PressAsync(action.PressKey);
354+
}
355+
}
356+
else if (action.Action == BroswerActionEnum.Hover)
357+
{
358+
await body.HoverAsync();
359+
}
360+
else if (action.Action == BroswerActionEnum.DragAndDrop)
361+
{
362+
// Locate the element to drag
363+
var box = await body.BoundingBoxAsync();
364+
365+
if (box != null)
366+
{
367+
// Calculate start position
368+
float startX = box.X + box.Width / 2; // Start at the center of the element
369+
float startY = box.Y + box.Height / 2;
370+
371+
// Drag offsets
372+
float offsetX = action.Position.X;
373+
// Move horizontally
374+
if (action.Position.Y == 0)
375+
{
376+
// Perform drag-and-move
377+
// Move mouse to the start position
378+
var mouse = page.Mouse;
379+
await mouse.MoveAsync(startX, startY);
380+
await mouse.DownAsync();
381+
382+
// Move mouse smoothly in increments
383+
var tracks = GetVelocityTrack(offsetX);
384+
foreach (var track in tracks)
385+
{
386+
startX += track;
387+
await page.Mouse.MoveAsync(startX, 0, new MouseMoveOptions
388+
{
389+
Steps = 3
390+
});
391+
}
392+
393+
// Release mouse button
394+
await Task.Delay(1000);
395+
await mouse.UpAsync();
396+
}
397+
else
398+
{
399+
throw new NotImplementedException();
400+
}
401+
}
402+
}
209403
}
404+
405+
210406
private void DeleteDirectory(string directory)
211407
{
212408
if (Directory.Exists(directory))

src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ public async Task<BrowserActionResult> LocateElement(MessageInfo message, Elemen
2121
IsSuccess = false
2222
};
2323
}
24+
25+
// Use IWebElementLocator to detect element position by element description
26+
var locators = _services.GetServices<IWebElementLocator>();
27+
foreach (var el in locators)
28+
{
29+
location.Position = await el.DetectElementCoordinates(this, message.ContextId, location.ElementLocatorDescription);
30+
31+
if (location.Position != null && location.Position.X > 0 && location.Position.Y > 0)
32+
{
33+
result.Message = $"Position based locating is found at {location.Position}";
34+
return new BrowserActionResult
35+
{
36+
IsSuccess = true
37+
};
38+
}
39+
}
40+
2441
ILocator locator = page.Locator("body");
2542
int count = 0;
2643
var keyword = string.Empty;

src/Plugins/BotSharp.Plugin.WebDriver/Hooks/WebUtilityHook.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class WebUtilityHook : IAgentUtilityHook
77
private const string GO_TO_PAGE_FN = $"{PREFIX}go_to_page";
88
private const string LOCATE_ELEMENT_FN = $"{PREFIX}locate_element";
99
private const string ACTION_ON_ELEMENT_FN = $"{PREFIX}action_on_element";
10+
private const string TAKE_SCREENSHOT_FN = $"{PREFIX}take_screenshot";
1011

1112
public void AddUtilities(List<AgentUtility> utilities)
1213
{
@@ -20,12 +21,10 @@ public void AddUtilities(List<AgentUtility> utilities)
2021
new UtilityItem
2122
{
2223
FunctionName = GO_TO_PAGE_FN,
23-
TemplateName = $"{GO_TO_PAGE_FN}.fn"
2424
},
2525
new UtilityItem
2626
{
2727
FunctionName = ACTION_ON_ELEMENT_FN,
28-
TemplateName = $"{ACTION_ON_ELEMENT_FN}.fn"
2928
},
3029
new UtilityItem
3130
{
@@ -34,6 +33,10 @@ public void AddUtilities(List<AgentUtility> utilities)
3433
new UtilityItem
3534
{
3635
FunctionName = CLOSE_BROWSER_FN
36+
},
37+
new UtilityItem
38+
{
39+
FunctionName = TAKE_SCREENSHOT_FN
3740
}
3841
]
3942
}

0 commit comments

Comments
 (0)