Merge pull request #811 from Qtoss-AI/master

WebDriver enhancement
This commit is contained in:
Haiping 2025-01-02 22:40:32 +00:00 committed by GitHub
commit ac78e6d3a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 339 additions and 190 deletions

View file

@ -28,4 +28,5 @@ public interface IWebBrowser
Task<BrowserActionResult> CloseCurrentPage(MessageInfo message);
Task<BrowserActionResult> SendHttpRequest(MessageInfo message, HttpRequestParams actionParams);
Task<BrowserActionResult> GetAttributeValue(MessageInfo message, ElementLocatingArgs location);
Task<BrowserActionResult> SetAttributeValue(MessageInfo message, ElementLocatingArgs location);
}

View file

@ -3,4 +3,6 @@ namespace BotSharp.Abstraction.Browsing.Models;
public class BrowserActionArgs
{
public bool Headless { get; set; }
public string? UserDataDir { get; set; }
public string? RemoteHostUrl { get; set; }
}

View file

@ -8,6 +8,10 @@ public class BrowserActionResult
public string? StackTrace { get; set; }
public string? Selector { get; set; }
public string? Body { get; set; }
/// <summary>
/// Page open in new tab after button click
/// </summary>
public string? UrlAfterAction { get; set; }
public bool IsHighlighted { get; set; }
public override string ToString()

View file

@ -33,6 +33,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
List<User> GetUserByIds(List<string> ids) => throw new NotImplementedException();
List<User> GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException();
User? GetUserByUserName(string userName) => throw new NotImplementedException();
void UpdateUserName(string userId, string userName) => throw new NotImplementedException();
Dashboard? GetDashboard(string id = null) => throw new NotImplementedException();
void CreateUser(User user) => throw new NotImplementedException();
void UpdateExistUser(string userId, User user) => throw new NotImplementedException();
@ -49,6 +50,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
PagedItems<User> GetUsers(UserFilter filter) => throw new NotImplementedException();
User? GetUserDetails(string userId, bool includeAgent = false) => throw new NotImplementedException();
bool UpdateUser(User user, bool updateUserAgents = false) => throw new NotImplementedException();
#endregion
#region Agent

View file

@ -17,6 +17,7 @@ public interface IUserService
Task<Token?> GetAffiliateToken(string authorization);
Task<Token?> GetAdminToken(string authorization);
Task<Token?> GetToken(string authorization);
Task<Token> CreateTokenByUser(User user);
Task<User> GetMyProfile();
Task<bool> VerifyUserNameExisting(string userName);
Task<bool> VerifyEmailExisting(string email);

View file

@ -23,7 +23,6 @@ public class User
public bool Verified { get; set; }
public string RegionCode { get; set; } = "CN";
public string? AffiliateId { get; set; }
public string? ReferralCode { get; set; }
public string? EmployeeId { get; set; }
public bool IsDisabled { get; set; }
public IEnumerable<string> Permissions { get; set; } = [];

View file

@ -4,4 +4,5 @@ public class UserActivationModel
{
public string UserName { get; set; }
public string VerificationCode { get; set; }
public string RegionCode { get; set; } = "CN";
}

View file

@ -190,10 +190,10 @@
<ItemGroup>
<PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="DistributedLock.Redis" Version="1.0.3" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.6.0" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.8.0" />
<PackageReference Include="Fluid.Core" Version="2.11.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Nanoid" Version="3.1.0" />
</ItemGroup>

View file

@ -39,10 +39,7 @@ public class RedisPublisher : IEventPublisher
// Add a message to the stream, keeping only the latest 1 million messages
var messageId = await db.StreamAddAsync(channel,
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
],
AssembleMessage(message),
maxLength: 1000 * 10000);
_logger.LogInformation($"Published message {channel} {message} ({messageId})");
@ -82,6 +79,25 @@ public class RedisPublisher : IEventPublisher
return exists;
}
public static NameValueEntry[] AssembleMessage(RedisValue message)
{
return
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
];
}
public static NameValueEntry[] AssembleErrorMessage(RedisValue message, string error)
{
return
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")),
new NameValueEntry("error", error)
];
}
public async Task ReDispatchAsync(string channel, int count = 10, string order = "asc")
{
var db = _redis.GetDatabase();
@ -93,10 +109,10 @@ public class RedisPublisher : IEventPublisher
try
{
var messageId = await db.StreamAddAsync(channel, [
new NameValueEntry("message", entry.Values[0].Value),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
]);
var message = entry.Values.First(x => x.Name == "message").Value;
var messageId = await db.StreamAddAsync(channel,
AssembleMessage(message),
maxLength: 1000 * 10000);
_logger.LogWarning($"ReDispatched message: {channel} {entry.Values[0].Value} ({messageId})");
@ -164,20 +180,7 @@ public class RedisPublisher : IEventPublisher
var db = _redis.GetDatabase();
var entries = await db.StreamRangeAsync(channel, "-", "+", count: count, messageOrder: Order.Ascending);
foreach (var entry in entries)
{
_logger.LogInformation($"Fetched message: {channel} {entry.Values[0].Value} ({entry.Id})");
try
{
await db.StreamDeleteAsync(channel, [entry.Id]);
_logger.LogWarning($"Deleted message: {channel} {entry.Values[0].Value} ({entry.Id})");
}
catch (Exception ex)
{
_logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}");
}
}
var deletedCount = await db.StreamDeleteAsync(channel, entries.Select(x => x.Id).ToArray());
_logger.LogWarning($"Deleted {deletedCount} messages from Redis stream {channel}");
}
}

View file

@ -42,6 +42,8 @@ public class RedisSubscriber : IEventSubscriber
await CreateConsumerGroup(db, channel, group);
}
await CreateConsumerGroup(db, $"{channel}-Error", group);
var consumer = Environment.MachineName;
if (port.HasValue)
{
@ -58,28 +60,36 @@ public class RedisSubscriber : IEventSubscriber
break;
}
if (priorityEnabled)
try
{
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received) > 0)
if (priorityEnabled)
{
continue;
}
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received, $"{channel}-Error") > 0)
{
continue;
}
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received) > 0)
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received, $"{channel}-Error") > 0)
{
continue;
}
await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received, $"{channel}-Error");
}
else
{
continue;
await HandleGroupMessage(db, channel, group, consumer, received, $"{channel}-Error");
}
await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received);
}
else
catch (Exception ex)
{
await HandleGroupMessage(db, channel, group, consumer, received);
_logger.LogError($"Error processing message: {ex.Message}\r\n{ex}");
await Task.Delay(1000 * 60);
}
}
}
private async Task<int> HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func<string, string, Task> received)
private async Task<int> HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func<string, string, Task> received, string errorChannel)
{
var entries = await db.StreamReadGroupAsync(channel, group, consumer, count: 1);
foreach (var entry in entries)
@ -90,13 +100,24 @@ public class RedisSubscriber : IEventSubscriber
try
{
await received(channel, entry.Values[0].Value);
// Optionally delete the message to save space
await db.StreamDeleteAsync(channel, [entry.Id]);
}
catch (Exception ex)
{
_logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}");
_logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id} {entry.Values[0].Value}");
// Add a message to the Error stream, keeping only the latest 1 million messages
await db.StreamAddAsync(errorChannel,
RedisPublisher.AssembleErrorMessage(entry.Values[0].Value, ex.Message),
messageId: entry.Id,
maxLength: 1000 * 10000);
// Slow down the consumer if there are errors
await Task.Delay(1000 * 10);
}
finally
{
var deletedCount = await db.StreamDeleteAsync(channel, [entry.Id]);
_logger.LogInformation($"Handled message {entry.Id}: {deletedCount == 1}");
}
}

View file

@ -62,7 +62,7 @@ public class UserService : IUserService
if (!string.IsNullOrWhiteSpace(user.Phone))
{
record = db.GetUserByPhone(user.Phone);
record = db.GetUserByPhone(user.Phone, regionCode: (string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode));
}
if (record == null && !string.IsNullOrWhiteSpace(user.Email))
@ -112,7 +112,7 @@ public class UserService : IUserService
db.UpdateExistUser(hasRegisterId, record);
}
_logger.LogWarning($"Created new user account: {record.Id} {record.UserName}");
_logger.LogWarning($"Created new user account: {record.Id} {record.UserName}, RegionCode: {record.RegionCode}");
Utilities.ClearCache();
var hooks = _services.GetServices<IAuthenticationHook>();
@ -127,7 +127,8 @@ public class UserService : IUserService
public async Task<bool> UpdatePassword(string password, string verificationCode)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetUserByUserName(_user.UserName);
var record = db.GetUserById(_user.Id);
if (record == null)
{
@ -473,7 +474,7 @@ public class UserService : IUserService
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
if (record == null)
{
record = db.GetUserByPhone(id);
record = db.GetUserByPhone(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode));
}
if (record == null)
@ -505,6 +506,20 @@ public class UserService : IUserService
return token;
}
public async Task<Token> CreateTokenByUser(User user)
{
var accessToken = GenerateJwtToken(user);
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
var token = new Token
{
AccessToken = accessToken,
ExpireTime = jwt.Payload.Exp.Value,
TokenType = "Bearer",
Scope = "api"
};
return token;
}
public async Task<bool> VerifyUserNameExisting(string userName)
{
if (string.IsNullOrEmpty(userName))
@ -646,7 +661,7 @@ public class UserService : IUserService
if (!string.IsNullOrEmpty(user.Phone))
{
record = db.GetUserByPhone(user.Phone);
record = db.GetUserByPhone(user.Phone, regionCode: (string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode));
}
if (record == null)
@ -695,7 +710,7 @@ public class UserService : IUserService
var curUser = await GetMyProfile();
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetUserById(curUser.Id);
var existPhone = db.GetUserByPhone(phone);
var existPhone = db.GetUserByPhone(phone, regionCode: regionCode);
if (record == null || (existPhone != null && existPhone.RegionCode == regionCode))
{

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Settings;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
@ -244,7 +243,7 @@ public class UserController : ControllerBase
var file = fileStorage.GetUserAvatar();
if (string.IsNullOrEmpty(file))
{
return NotFound();
return NoContent();
}
return BuildFileResult(file);
}

View file

@ -13,7 +13,6 @@ public class UserCreationModel
public string Type { get; set; } = UserType.Client;
public string Role { get; set; } = UserRole.User;
public string RegionCode { get; set; } = "CN";
public string? ReferralCode { get; set; }
public User ToUser()
{
return new User
@ -27,7 +26,6 @@ public class UserCreationModel
Role = Role,
Type = Type,
RegionCode = RegionCode,
ReferralCode = ReferralCode
};
}
}

View file

@ -33,10 +33,6 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
</ItemGroup>
<ItemGroup>

View file

@ -36,7 +36,7 @@ public class MongoDbContext
Key = x.Split("=")[0],
Value = x.Split("=")[1]
}).ToList();
var source = queries.FirstOrDefault(x => x.Key.IsEqualTo(DB_NAME_INDEX));
if (source != null)
{
@ -168,4 +168,5 @@ public class MongoDbContext
public IMongoCollection<CrontabItemDocument> CrontabItems
=> Database.GetCollection<CrontabItemDocument>($"{_collectionPrefix}_CronTabItems");
}

View file

@ -112,6 +112,14 @@ public partial class MongoRepository
_dc.Users.UpdateOne(filter, update);
}
public void UpdateUserName(string userId, string userName)
{
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
var update = Builders<UserDocument>.Update
.Set(x => x.UserName, userName);
_dc.Users.UpdateOne(filter, update);
}
public void UpdateUserVerified(string userId)
{
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
@ -151,7 +159,7 @@ public partial class MongoRepository
var update = Builders<UserDocument>.Update.Set(x => x.Phone, phone)
.Set(x => x.UpdatedTime, DateTime.UtcNow)
.Set(x => x.RegionCode, regionCode)
.Set(x => x.UserName, phone)
//.Set(x => x.UserName, phone)
.Set(x => x.FirstName, phone);
_dc.Users.UpdateOne(filter, update);
}
@ -331,10 +339,10 @@ public partial class MongoRepository
.FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId));
if (user == null) return;
var curDash = user.Dashboard ?? new Dashboard();
curDash.ConversationList.Add(new DashboardConversation
{
curDash.ConversationList.Add(new DashboardConversation
{
Id = Guid.NewGuid().ToString(),
ConversationId = conversationId
ConversationId = conversationId
});
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.WebDriver" Version="4.27.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -1,7 +1,7 @@
using OpenQA.Selenium.Chrome;
using System.IO;
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public class SeleniumInstance : IDisposable
{

View file

@ -1,7 +1,6 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,4 +1,4 @@
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,4 +1,4 @@
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,4 +1,4 @@
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,6 +1,6 @@
using System.Net.Http;
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,4 +1,4 @@
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,7 +1,6 @@
using OpenQA.Selenium;
using System.Collections.ObjectModel;
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver
{

View file

@ -1,4 +1,4 @@
namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver;
namespace BotSharp.Plugin.Selenium.Drivers;
public partial class SeleniumWebDriver : IWebBrowser
{
@ -86,4 +86,9 @@ public partial class SeleniumWebDriver : IWebBrowser
{
throw new NotImplementedException();
}
public Task PressKey(MessageInfo message, string key)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,23 @@
global using System;
global using System.Collections.Generic;
global using System.Text;
global using System.Threading.Tasks;
global using System.Text.Json;
global using System.Linq;
global using System.Text.RegularExpressions;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using BotSharp.Abstraction.Browsing.Enums;
global using BotSharp.Abstraction.Conversations;
global using BotSharp.Abstraction.Plugins;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Functions;
global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Abstraction.Agents;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Abstraction.Browsing.Models;
global using BotSharp.Abstraction.Browsing;
global using OpenQA.Selenium;

View file

@ -1,13 +1,9 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Messaging.Enums;
using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Planning;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using System.Text.RegularExpressions;
using BotSharp.Plugin.SqlDriver.Interfaces;
namespace BotSharp.Plugin.SqlDriver.Hooks;

View file

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
@ -11,16 +11,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Drivers\SeleniumDriver\**" />
<Compile Remove="packages\**" />
<EmbeddedResource Remove="Drivers\SeleniumDriver\**" />
<EmbeddedResource Remove="packages\**" />
<None Remove="Drivers\SeleniumDriver\**" />
<None Remove="packages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Playwright" Version="1.48.0" />
<PackageReference Include="Microsoft.Playwright" Version="1.49.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
</ItemGroup>

View file

@ -45,28 +45,36 @@ public class PlaywrightInstance : IDisposable
_playwright = await Playwright.CreateAsync();
}
string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{ctxId}";
_contexts[ctxId] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions
if (!string.IsNullOrEmpty(args.RemoteHostUrl))
{
Headless = args.Headless,
Channel = "chrome",
ViewportSize = new ViewportSize
var browser = await _playwright.Chromium.ConnectOverCDPAsync(args.RemoteHostUrl);
_contexts[ctxId] = browser.Contexts[0];
}
else
{
string userDataDir = args.UserDataDir ?? $"{Path.GetTempPath()}\\playwright\\{ctxId}";
_contexts[ctxId] = await _playwright.Chromium.LaunchPersistentContextAsync(userDataDir, new BrowserTypeLaunchPersistentContextOptions
{
Width = 1600,
Height = 900
},
IgnoreDefaultArgs =
[
"--enable-automation",
],
Args =
[
"--disable-infobars",
"--test-type"
// "--start-maximized"
]
});
Headless = args.Headless,
Channel = "chrome",
ViewportSize = new ViewportSize
{
Width = 1600,
Height = 900
},
IgnoreDefaultArgs =
[
"--enable-automation",
],
Args =
[
"--disable-infobars",
"--test-type"
// "--start-maximized"
]
});
}
_pages[ctxId] = new List<IPage>();
_contexts[ctxId].Page += async (sender, page) =>
@ -100,12 +108,7 @@ public class PlaywrightInstance : IDisposable
return _contexts[ctxId];
}
public async Task<IPage> NewPage(MessageInfo message,
bool enableResponseCallback = false,
bool responseInMemory = false,
List<WebPageResponseData>? responseContainer = null,
string[]? excludeResponseUrls = null,
string[]? includeResponseUrls = null)
public async Task<IPage> NewPage(MessageInfo message, PageActionArgs args)
{
var context = await GetContext(message.ContextId);
var page = await context.NewPageAsync();
@ -115,70 +118,78 @@ public class PlaywrightInstance : IDisposable
var js = @"Object.defineProperties(navigator, {webdriver:{get:()=>false}});";
await page.AddInitScriptAsync(js);
if (!enableResponseCallback)
if (!args.EnableResponseCallback)
{
return page;
}
page.Response += async (sender, e) =>
{
if (e.Status != 204 &&
e.Headers.ContainsKey("content-type") &&
(e.Request.ResourceType == "fetch" || e.Request.ResourceType == "xhr") &&
(excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))) &&
(includeResponseUrls == null || includeResponseUrls.Any(url => e.Url.ToLower().Contains(url))))
{
Serilog.Log.Information($"{e.Request.Method}: {e.Url}");
try
{
var result = new WebPageResponseData
{
Url = e.Url.ToLower(),
PostData = e.Request?.PostData ?? string.Empty,
ResponseInMemory = responseInMemory
};
if (e.Headers["content-type"].Contains("application/json"))
{
if (e.Status == 200 && e.Ok)
{
var json = await e.JsonAsync();
result.ResponseData = JsonSerializer.Serialize(json);
}
}
else
{
var html = await e.TextAsync();
result.ResponseData = html;
}
if (responseContainer != null && responseInMemory)
{
responseContainer.Add(result);
}
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
var webPageResponseHooks = _services.GetServices<IWebPageResponseHook>();
foreach (var hook in webPageResponseHooks)
{
hook.OnDataFetched(message, result);
}
}
catch (ObjectDisposedException ex)
{
Serilog.Log.Information(ex.Message);
}
catch (Exception ex)
{
Serilog.Log.Error($"{e.Url}\r\n" + ex.ToString());
}
}
await HandleFetchResponse(e, message, args);
};
return page;
}
public async Task HandleFetchResponse(IResponse response, MessageInfo message, PageActionArgs args)
{
if (response.Status != 204 &&
response.Headers.ContainsKey("content-type") &&
(response.Request.ResourceType == "fetch" || response.Request.ResourceType == "xhr") &&
(args.ExcludeResponseUrls == null || !args.ExcludeResponseUrls.Any(url => response.Url.ToLower().Contains(url))) &&
(args.IncludeResponseUrls == null || args.IncludeResponseUrls.Any(url => response.Url.ToLower().Contains(url))))
{
Serilog.Log.Information($"{response.Request.Method}: {response.Url}");
try
{
var result = new WebPageResponseData
{
Url = response.Url.ToLower(),
PostData = response.Request?.PostData ?? string.Empty,
ResponseInMemory = args.ResponseInMemory
};
var html = await response.TextAsync();
if (response.Headers["content-type"].Contains("application/json"))
{
if (response.Status == 200 && response.Ok)
{
if (!string.IsNullOrWhiteSpace(html))
{
var json = await response.JsonAsync();
result.ResponseData = JsonSerializer.Serialize(json);
}
}
}
else
{
result.ResponseData = html;
}
if (args.ResponseContainer != null && args.ResponseInMemory)
{
args.ResponseContainer.Add(result);
}
Serilog.Log.Warning($"Response status: {response.Status} {response.StatusText}, OK: {response.Ok}");
var webPageResponseHooks = _services.GetServices<IWebPageResponseHook>();
foreach (var hook in webPageResponseHooks)
{
hook.OnDataFetched(message, result);
}
}
catch (ObjectDisposedException ex)
{
Serilog.Log.Information(ex.Message);
}
catch (Exception ex)
{
Serilog.Log.Error($"{response.Url}\r\n" + ex.ToString());
}
}
}
/// <summary>
/// Wait page and network until timeout in seconds
/// </summary>

View file

@ -9,6 +9,7 @@ public partial class PlaywrightWebDriver
if (result.IsSuccess)
{
await DoAction(message, action, result);
result.UrlAfterAction = _instance.GetPage(message.ContextId)?.Url;
}
return result;
}

View file

@ -113,6 +113,7 @@ public partial class PlaywrightWebDriver
}
// Release mouse button
await Task.Delay(1000);
await mouse.UpAsync();
}
else

View file

@ -19,4 +19,25 @@ public partial class PlaywrightWebDriver
Body = value ?? string.Empty
};
}
public async Task<BrowserActionResult> SetAttributeValue(MessageInfo message, ElementLocatingArgs location)
{
var page = _instance.GetPage(message.ContextId);
ILocator locator = page.Locator(location.Selector);
var elementCount = await locator.CountAsync();
if (elementCount > 0)
{
foreach (var element in await locator.AllAsync())
{
var script = $"element => element.{location.AttributeName} = '{location.AttributeValue}'";
var result = await locator.EvaluateAsync(script);
}
}
return new BrowserActionResult
{
IsSuccess = true
};
}
}

View file

@ -8,32 +8,41 @@ public partial class PlaywrightWebDriver
var context = await _instance.GetContext(message.ContextId);
try
{
var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
excludeResponseUrls: args.ExcludeResponseUrls,
includeResponseUrls: args.IncludeResponseUrls);
Serilog.Log.Information($"goto page: {args.Url}");
if (args.OpenNewTab && page != null && page.Url == "about:blank")
IPage? page = null;
if (!args.OpenNewTab)
{
page = await _instance.NewPage(message,
enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
excludeResponseUrls: args.ExcludeResponseUrls,
includeResponseUrls: args.IncludeResponseUrls);
page = _instance.Contexts[message.ContextId].Pages.LastOrDefault();
if (page != null)
{
await page.EvaluateAsync(@"() => {
window.open('', '_blank');
}");
if (args.EnableResponseCallback)
{
page.Response += async (sender, e) =>
{
await _instance.HandleFetchResponse(e, message, args);
};
}
}
}
else
{
page = await _instance.NewPage(message, args);
Serilog.Log.Information($"goto page: {args.Url}");
if (args.OpenNewTab && page != null && page.Url == "about:blank")
{
page = await _instance.NewPage(message, args);
}
}
if (page == null)
{
page = await _instance.NewPage(message,
enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
excludeResponseUrls: args.ExcludeResponseUrls,
includeResponseUrls: args.IncludeResponseUrls);
page = await _instance.NewPage(message, args);
}
// Active current tab

View file

@ -14,6 +14,13 @@ public partial class PlaywrightWebDriver
{
var result = new BrowserActionResult();
var page = _instance.GetPage(message.ContextId);
if (page == null)
{
return new BrowserActionResult
{
IsSuccess = false
};
}
ILocator locator = page.Locator("body");
int count = 0;

View file

@ -1,5 +1,3 @@
using SQLitePCL;
namespace BotSharp.Plugin.WebDriver.Functions;
public class OpenBrowserFn : IFunctionCallback

View file

@ -1,10 +1,25 @@
using HtmlAgilityPack;
using System.Net.Http;
namespace BotSharp.Plugin.WebDriver;
public static class WebPageHelper
{
public static HtmlNode GetElement(string html, string selector)
{
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
return htmlDoc.DocumentNode.SelectSingleNode(selector);
}
public static HtmlNodeCollection GetElements(string html, string selector)
{
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
return htmlDoc.DocumentNode.SelectNodes(selector);
}
public static string RemoveElements(string html, string selector)
{
var htmlDoc = new HtmlDocument();