2025-03-28 16:32:41 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
|
|
2024-03-08 13:17:36 +00:00
|
|
|
|
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
|
|
|
|
|
|
|
|
|
|
|
|
public partial class PlaywrightWebDriver
|
|
|
|
|
|
{
|
|
|
|
|
|
public async Task DoAction(MessageInfo message, ElementActionArgs action, BrowserActionResult result)
|
|
|
|
|
|
{
|
2024-04-09 03:38:56 +00:00
|
|
|
|
var page = _instance.GetPage(message.ContextId);
|
2024-07-02 14:52:16 +00:00
|
|
|
|
if (string.IsNullOrEmpty(result.Selector))
|
|
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Error($"Selector is not set.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 12:47:58 +00:00
|
|
|
|
ILocator locator = page.Locator(result.Selector);
|
2024-07-02 14:52:16 +00:00
|
|
|
|
var count = await locator.CountAsync();
|
2024-08-22 02:07:50 +00:00
|
|
|
|
|
2024-07-02 14:52:16 +00:00
|
|
|
|
if (count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Error($"Element not found: {result.Selector}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-08-22 02:07:50 +00:00
|
|
|
|
else if (count > 1)
|
|
|
|
|
|
{
|
2025-06-13 16:08:53 +00:00
|
|
|
|
if (!action.FirstIfMultipleFound)
|
2024-08-22 02:07:50 +00:00
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Error($"Multiple eElements were found: {result.Selector}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
locator = page.Locator(result.Selector).First;// 匹配到多个时取第一个,否则当await locator.ClickAsync();匹配到多个就会抛异常。
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-08 13:17:36 +00:00
|
|
|
|
|
2024-04-08 15:09:08 +00:00
|
|
|
|
if (action.Action == BroswerActionEnum.Click)
|
2024-03-08 13:17:36 +00:00
|
|
|
|
{
|
2024-04-16 04:48:22 +00:00
|
|
|
|
if (action.Position == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.ClickAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.ClickAsync(new LocatorClickOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Position = new Position
|
|
|
|
|
|
{
|
|
|
|
|
|
X = action.Position.X,
|
|
|
|
|
|
Y = action.Position.Y
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-03-08 13:17:36 +00:00
|
|
|
|
}
|
2025-03-28 16:32:41 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.DropDown)
|
|
|
|
|
|
{
|
2025-07-27 19:04:29 +00:00
|
|
|
|
var tagName = await locator.EvaluateAsync<string>("el => el.tagName.toLowerCase()");
|
|
|
|
|
|
if (tagName == "select")
|
2025-03-28 16:32:41 +00:00
|
|
|
|
{
|
2025-07-27 19:04:29 +00:00
|
|
|
|
await HandleSelectDropDownAsync(page, locator, action);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.ClickAsync();
|
2025-08-20 19:11:10 +00:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(action.PressKey))
|
2025-07-27 19:04:29 +00:00
|
|
|
|
{
|
2025-08-20 19:11:10 +00:00
|
|
|
|
await page.Keyboard.PressAsync(action.PressKey);
|
|
|
|
|
|
await page.Keyboard.PressAsync("Enter");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var optionLocator = page.Locator($"//div[text()='{action.Content}']");
|
|
|
|
|
|
var optionCount = await optionLocator.CountAsync();
|
|
|
|
|
|
if (optionCount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Error($"Dropdown option not found: {action.Content}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await optionLocator.First.ClickAsync();
|
2025-07-27 19:04:29 +00:00
|
|
|
|
}
|
2025-03-28 16:32:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-04-08 15:09:08 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.InputText)
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.FillAsync(action.Content);
|
2024-05-17 23:45:18 +00:00
|
|
|
|
|
|
|
|
|
|
if (action.PressKey != null)
|
|
|
|
|
|
{
|
2024-11-18 18:52:59 +00:00
|
|
|
|
if (action.DelayBeforePressingKey > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Delay(action.DelayBeforePressingKey);
|
|
|
|
|
|
}
|
2024-05-17 23:45:18 +00:00
|
|
|
|
await locator.PressAsync(action.PressKey);
|
|
|
|
|
|
}
|
2024-04-08 15:09:08 +00:00
|
|
|
|
}
|
2025-03-28 16:32:41 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.FileUpload)
|
|
|
|
|
|
{
|
2025-04-18 21:12:30 +00:00
|
|
|
|
var _states = _services.GetRequiredService<IConversationStateService>();
|
|
|
|
|
|
var files = new List<string>();
|
|
|
|
|
|
if (action.FileUrl != null && action.FileUrl.Length > 0)
|
2025-03-28 16:32:41 +00:00
|
|
|
|
{
|
2025-04-18 21:12:30 +00:00
|
|
|
|
files.AddRange(action.FileUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
var hooks = _services.GetServices<IWebDriverHook>();
|
|
|
|
|
|
foreach (var hook in hooks)
|
|
|
|
|
|
{
|
|
|
|
|
|
files.AddRange(await hook.GetUploadFiles(message));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (files.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Warning($"No files found to upload: {action.Content}");
|
2025-03-28 16:32:41 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var fileChooser = await page.RunAndWaitForFileChooserAsync(async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.ClickAsync();
|
|
|
|
|
|
});
|
|
|
|
|
|
var guid = Guid.NewGuid().ToString();
|
|
|
|
|
|
var directory = Path.Combine(Path.GetTempPath(), guid);
|
2025-06-13 16:08:53 +00:00
|
|
|
|
DeleteDirectory(directory);
|
2025-03-28 16:32:41 +00:00
|
|
|
|
Directory.CreateDirectory(directory);
|
|
|
|
|
|
var localPaths = new List<string>();
|
2025-06-13 16:08:53 +00:00
|
|
|
|
var http = _services.GetRequiredService<IHttpClientFactory>();
|
|
|
|
|
|
using var httpClient = http.CreateClient();
|
2025-04-18 21:12:30 +00:00
|
|
|
|
foreach (var fileUrl in files)
|
2025-03-28 16:32:41 +00:00
|
|
|
|
{
|
2025-06-13 16:08:53 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var fileData = await httpClient.GetAsync(fileUrl);
|
|
|
|
|
|
var fileName = new Uri(fileUrl).AbsolutePath;
|
|
|
|
|
|
var localPath = Path.Combine(directory, Path.GetFileName(fileName));
|
|
|
|
|
|
await using var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
|
|
|
|
await fileData.Content.CopyToAsync(fs);
|
|
|
|
|
|
localPaths.Add(localPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Serilog.Log.Error($"FileUpload failed for {fileUrl}. Message: {ex.Message}");
|
|
|
|
|
|
}
|
2025-03-28 16:32:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
await fileChooser.SetFilesAsync(localPaths);
|
2025-06-13 16:08:53 +00:00
|
|
|
|
await Task.Delay(1000 * action.WaitTime);
|
2025-03-28 16:32:41 +00:00
|
|
|
|
}
|
2024-06-02 02:15:47 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.Typing)
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.PressSequentiallyAsync(action.Content);
|
|
|
|
|
|
if (action.PressKey != null)
|
|
|
|
|
|
{
|
2024-11-18 18:52:59 +00:00
|
|
|
|
if (action.DelayBeforePressingKey > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Delay(action.DelayBeforePressingKey);
|
|
|
|
|
|
}
|
2024-06-02 02:15:47 +00:00
|
|
|
|
await locator.PressAsync(action.PressKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-06-10 15:51:14 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.Hover)
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.HoverAsync();
|
|
|
|
|
|
}
|
2024-11-24 02:02:43 +00:00
|
|
|
|
else if (action.Action == BroswerActionEnum.DragAndDrop)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Locate the element to drag
|
|
|
|
|
|
var box = await locator.BoundingBoxAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (box != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Calculate start position
|
|
|
|
|
|
float startX = box.X + box.Width / 2; // Start at the center of the element
|
|
|
|
|
|
float startY = box.Y + box.Height / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// Drag offsets
|
|
|
|
|
|
float offsetX = action.Position.X;
|
|
|
|
|
|
// Move horizontally
|
|
|
|
|
|
if (action.Position.Y == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Perform drag-and-move
|
|
|
|
|
|
// Move mouse to the start position
|
|
|
|
|
|
var mouse = page.Mouse;
|
2025-06-13 16:08:53 +00:00
|
|
|
|
await mouse.MoveAsync(startX, startY);
|
|
|
|
|
|
await mouse.DownAsync();
|
2024-11-24 02:02:43 +00:00
|
|
|
|
|
|
|
|
|
|
// Move mouse smoothly in increments
|
|
|
|
|
|
var tracks = GetVelocityTrack(offsetX);
|
|
|
|
|
|
foreach (var track in tracks)
|
|
|
|
|
|
{
|
|
|
|
|
|
startX += track;
|
|
|
|
|
|
await page.Mouse.MoveAsync(startX, 0, new MouseMoveOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Steps = 3
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Release mouse button
|
2024-12-11 21:16:42 +00:00
|
|
|
|
await Task.Delay(1000);
|
2024-11-24 02:02:43 +00:00
|
|
|
|
await mouse.UpAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new NotImplementedException();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-01 18:18:34 +00:00
|
|
|
|
|
|
|
|
|
|
if (action.WaitTime > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Delay(1000 * action.WaitTime);
|
|
|
|
|
|
}
|
2024-03-08 13:17:36 +00:00
|
|
|
|
}
|
2025-06-13 16:08:53 +00:00
|
|
|
|
private void DeleteDirectory(string directory)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Directory.Exists(directory))
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.Delete(directory, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-11-24 02:02:43 +00:00
|
|
|
|
public static List<int> GetVelocityTrack(float distance)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Initialize the track list to store the movement distances
|
|
|
|
|
|
List<int> track = new List<int>();
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize variables
|
|
|
|
|
|
float current = 0; // Current position
|
|
|
|
|
|
float mid = distance * 4 / 5; // Deceleration threshold
|
|
|
|
|
|
float t = 0.2f; // Time interval
|
|
|
|
|
|
float v = 1; // Initial velocity
|
|
|
|
|
|
|
|
|
|
|
|
// Generate the track
|
|
|
|
|
|
while (current < distance)
|
|
|
|
|
|
{
|
|
|
|
|
|
float a; // Acceleration
|
|
|
|
|
|
|
|
|
|
|
|
// Determine acceleration based on position
|
|
|
|
|
|
if (current < mid)
|
|
|
|
|
|
{
|
|
|
|
|
|
a = 4; // Accelerate
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
a = -3; // Decelerate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate new velocity
|
|
|
|
|
|
float v0 = v;
|
|
|
|
|
|
v = v0 + a * t;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the movement during this interval
|
|
|
|
|
|
float move = v0 * t + 0.5f * a * t * t;
|
|
|
|
|
|
|
|
|
|
|
|
// Update current position
|
|
|
|
|
|
if (current + move > distance)
|
|
|
|
|
|
{
|
|
|
|
|
|
move = distance - current;
|
|
|
|
|
|
track.Add((int)Math.Round(move));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
current += move;
|
|
|
|
|
|
|
|
|
|
|
|
// Add rounded movement to the track
|
|
|
|
|
|
track.Add((int)Math.Round(move));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return track;
|
|
|
|
|
|
}
|
2025-07-27 19:04:29 +00:00
|
|
|
|
|
|
|
|
|
|
private async Task HandleSelectDropDownAsync(IPage page, ILocator locator, ElementActionArgs action)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(action.Content))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("Dropdown target option (action.Content) cannot be null or empty when using a <select> element.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var selectedText = await locator.Locator("option:checked").InnerTextAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.Equals(selectedText?.Trim(), action.Content.Trim(), StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
await locator.SelectOptionAsync(new SelectOptionValue
|
|
|
|
|
|
{
|
|
|
|
|
|
Label = action.Content
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
|
|
_logger.LogInformation($"Dropdown changed to: {action.Content}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation($"Dropdown already on correct option: {action.Content}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-08 13:17:36 +00:00
|
|
|
|
}
|