This commit is contained in:
Haiping Chen 2024-12-03 16:56:10 -06:00
commit bca3264ff3
25 changed files with 454 additions and 79 deletions

View file

@ -7,4 +7,5 @@ public enum BroswerActionEnum
Typing = 3,
Hover = 4,
Scroll = 5,
DragAndDrop = 6
}

View file

@ -5,4 +5,9 @@ public class ElementPosition
public float X { get; set; } = default!;
public float Y { get; set; } = default!;
public override string ToString()
{
return $"[{X}, {Y}]";
}
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Infrastructures.Enums;
public enum EventPriority
{
Low = 1,
Medium = 2,
High = 3
}

View file

@ -10,7 +10,11 @@ public interface IEventPublisher
/// <returns></returns>
Task BroadcastAsync(string channel, string message);
Task PublishAsync(string channel, string message);
Task<string?> PublishAsync(string channel, string message, EventPriority? priority = null);
Task ReDispatchAsync(string channel, int count = 10, string order = "asc");
Task ReDispatchPendingAsync(string channel, string group, int count = 10);
Task RemoveAsync(string channel, int count = 10);
}

View file

@ -1,8 +1,12 @@
using System.Threading;
namespace BotSharp.Abstraction.Infrastructures.Events;
public interface IEventSubscriber
{
Task SubscribeAsync(string channel, Func<string, string, Task> received);
Task SubscribeAsync(string channel, string group, Func<string, string, Task> received);
Task SubscribeAsync(string channel, string group, int? port, bool priorityEnabled,
Func<string, string, Task> received,
CancellationToken? stoppingToken = null);
}

View file

@ -50,7 +50,9 @@ public class SharpCacheAttribute : MoAttribute
}
var httpContext = Services.GetRequiredService<IHttpContextAccessor>();
if (httpContext.HttpContext.Response.Headers["Cache-Control"].ToString().Contains("no-store"))
if (httpContext != null &&
httpContext.HttpContext != null &&
httpContext.HttpContext.Response.Headers["Cache-Control"].ToString().Contains("no-store"))
{
return;
}

View file

@ -2,18 +2,19 @@ namespace BotSharp.Abstraction.Repositories;
public class BotSharpDatabaseSettings : DatabaseBasicSettings
{
public string[] Assemblies { get; set; }
public string FileRepository { get; set; }
public string BotSharpMongoDb { get; set; }
public string TablePrefix { get; set; }
public DbConnectionSetting BotSharp { get; set; }
public string Redis { get; set; }
public string[] Assemblies { get; set; } = [];
public string FileRepository { get; set; } = string.Empty;
public string BotSharpMongoDb { get; set; } = string.Empty;
public string TablePrefix { get; set; } = string.Empty;
public DbConnectionSetting BotSharp { get; set; } = new();
public string Redis { get; set; } = string.Empty;
public bool EnableReplica { get; set; } = true;
}
public class DatabaseBasicSettings
{
public string Default { get; set; }
public DbConnectionSetting DefaultConnection { get; set; }
public string Default { get; set; } = string.Empty;
public DbConnectionSetting DefaultConnection { get; set; } = new();
public bool EnableSqlLog { get; set; }
public bool EnableSensitiveDataLogging { get; set; }
public bool EnableRetryOnFailure { get; set; }
@ -23,9 +24,11 @@ public class DbConnectionSetting
{
public string Master { get; set; }
public string[] Slavers { get; set; }
public int ConnectionTimeout { get; set; } = 30;
public int ExecutionTimeout { get; set; } = 30;
public DbConnectionSetting()
{
Slavers = new string[0];
Slavers = [];
}
}

View file

@ -5,6 +5,7 @@ using BotSharp.Abstraction.Roles.Models;
using BotSharp.Abstraction.Shared;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Translation.Models;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
using BotSharp.Abstraction.VectorStorage.Models;
@ -26,7 +27,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
#region User
User? GetUserByEmail(string email) => throw new NotImplementedException();
User? GetUserByPhone(string phone, string role = null, string regionCode = "CN") => throw new NotImplementedException();
User? GetUserByPhone(string phone, string type = UserType.Client, string regionCode = "CN") => throw new NotImplementedException();
User? GetAffiliateUserByPhone(string phone) => throw new NotImplementedException();
User? GetUserById(string id) => throw new NotImplementedException();
List<User> GetUserByIds(List<string> ids) => throw new NotImplementedException();

View file

@ -23,6 +23,7 @@ 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

@ -0,0 +1,14 @@
namespace BotSharp.Abstraction.Utilities;
public static class MathExt
{
public static int Max(int a, int b, int c)
{
return Math.Max(Math.Max(a, b), c);
}
public static long Max(long a, long b, long c)
{
return Math.Max(Math.Max(a, b), c);
}
}

View file

@ -1,7 +1,9 @@
using BotSharp.Abstraction.Infrastructures;
using System.Diagnostics;
namespace BotSharp.Abstraction.Utilities;
[DebuggerStepThrough]
public class Pagination : ICacheKey
{
private int _page;

View file

@ -190,7 +190,7 @@
<ItemGroup>
<PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="DistributedLock.Redis" Version="1.0.3" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.5.1" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.6.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" />

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Infrastructures.Enums;
using StackExchange.Redis;
namespace BotSharp.Core.Infrastructures.Events;
@ -20,14 +21,65 @@ public class RedisPublisher : IEventPublisher
await _subscriber.PublishAsync(channel, message);
}
public async Task PublishAsync(string channel, string message)
public async Task<string?> PublishAsync(string channel, string message, EventPriority? priority = null)
{
var db = _redis.GetDatabase();
// convert to apporiate channel by priority
if (priority != null)
{
channel = $"{channel}-{priority}";
}
if (CheckMessageExists(db, channel, "message", message))
{
_logger.LogError($"The message already exists {channel} {message}");
return null;
}
// Add a message to the stream, keeping only the latest 1 million messages
await db.StreamAddAsync(channel, "message", message,
var messageId = await db.StreamAddAsync(channel,
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
],
maxLength: 1000 * 10000);
_logger.LogInformation($"Published message {channel} {message}");
_logger.LogInformation($"Published message {channel} {message} ({messageId})");
return messageId;
}
private bool CheckMessageExists(IDatabase db, string channel, string fieldName, string desiredValue)
{
// Define the range to fetch all messages
RedisValue start = "-"; // Start from the smallest ID
RedisValue end = "+"; // End at the largest ID
int count = 10; // Number of messages to retrieve
// Fetch the latest 10 messages
var streamEntries = db.StreamRange(channel, start, end, count, Order.Descending);
if (streamEntries.Length == 0)
{
return false;
}
// Check if any message contains the specific field value
bool exists = streamEntries.Any(entry =>
{
// Each entry contains a collection of Name-Value pairs
foreach (var nameValue in entry.Values)
{
if (nameValue.Name == fieldName && nameValue.Value == desiredValue)
{
return true;
}
}
return false;
});
return exists;
}
public async Task ReDispatchAsync(string channel, int count = 10, string order = "asc")
@ -41,7 +93,10 @@ public class RedisPublisher : IEventPublisher
try
{
var messageId = await db.StreamAddAsync(channel, "message", entry.Values[0].Value);
var messageId = await db.StreamAddAsync(channel, [
new NameValueEntry("message", entry.Values[0].Value),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
]);
_logger.LogWarning($"ReDispatched message: {channel} {entry.Values[0].Value} ({messageId})");
@ -50,7 +105,78 @@ public class RedisPublisher : IEventPublisher
}
catch (Exception ex)
{
_logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}");
_logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}");
}
}
}
public async Task ReDispatchPendingAsync(string channel, string group, int count = 10)
{
var db = _redis.GetDatabase();
// Step 1: Fetch pending messages using XPending
try
{
var pendingInfo = await db.StreamPendingAsync(channel, group);
if (pendingInfo.PendingMessageCount == 0)
{
Console.WriteLine("No pending messages found.");
return;
}
// Step 2: Fetch pending message IDs
List<StreamEntry> pendingMessages = new List<StreamEntry>();
foreach (var consumer in pendingInfo.Consumers)
{
var pendingMessageIds = await db.StreamPendingMessagesAsync(channel, group, count: consumer.PendingMessageCount, consumerName: consumer.Name);
var messageIds = pendingMessageIds.Select(x => x.MessageId).ToArray();
// Step 3: Use XClaim to fetch the actual message
var claimedMessages = await db.StreamClaimAsync(channel, group, consumer.Name, minIdleTimeInMs: 60 * 1000, messageIds);
pendingMessages.AddRange(claimedMessages);
await db.StreamAcknowledgeAsync(channel, group, messageIds);
}
// Step 4: Process the messages
foreach (var message in pendingMessages)
{
/*if (message.IsNull)
{
await db.StreamAcknowledgeAsync(channel, group, [message.Id]);
}
else
{
var messageId = await db.StreamAddAsync(channel, "message", message.Values[0].Value);
_logger.LogWarning($"ReDispatched message: {channel} {message.Values[0].Value} ({messageId})");
await db.StreamDeleteAsync(channel, [messageId]);
}*/
}
}
catch (RedisException ex)
{
Console.WriteLine($"Redis error: {ex.Message}");
}
}
public async Task RemoveAsync(string channel, int count = 10)
{
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}");
}
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Infrastructures.Enums;
using StackExchange.Redis;
namespace BotSharp.Core.Infrastructures.Events;
@ -24,10 +25,86 @@ public class RedisSubscriber : IEventSubscriber
});
}
public async Task SubscribeAsync(string channel, string group, Func<string, string, Task> received)
public async Task SubscribeAsync(string channel, string group, int? port, bool priorityEnabled,
Func<string, string, Task> received,
CancellationToken? stoppingToken = null)
{
var db = _redis.GetDatabase();
if (priorityEnabled)
{
await CreateConsumerGroup(db, $"{channel}-{EventPriority.Low}", group);
await CreateConsumerGroup(db, $"{channel}-{EventPriority.Medium}", group);
await CreateConsumerGroup(db, $"{channel}-{EventPriority.High}", group);
}
else
{
await CreateConsumerGroup(db, channel, group);
}
var consumer = Environment.MachineName;
if (port.HasValue)
{
consumer += $"-{port}";
}
while (true)
{
await Task.Delay(100);
if (stoppingToken.HasValue && stoppingToken.Value.IsCancellationRequested)
{
_logger.LogInformation($"Stopping consumer channel & group: [{channel}, {group}]");
break;
}
if (priorityEnabled)
{
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received) > 0)
{
continue;
}
if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received) > 0)
{
continue;
}
await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received);
}
else
{
await HandleGroupMessage(db, channel, group, consumer, received);
}
}
}
private async Task<int> HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func<string, string, Task> received)
{
var entries = await db.StreamReadGroupAsync(channel, group, consumer, count: 1);
foreach (var entry in entries)
{
_logger.LogInformation($"Consumer {Environment.MachineName} received: {channel} {entry.Values[0].Value}");
await db.StreamAcknowledgeAsync(channel, group, entry.Id);
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}");
}
}
return entries.Length;
}
private async Task CreateConsumerGroup(IDatabase db, string channel, string group)
{
// Create the consumer group if it doesn't exist
try
{
@ -43,33 +120,5 @@ public class RedisSubscriber : IEventSubscriber
_logger.LogError($"Error creating consumer group: '{group}' {ex.Message}");
throw;
}
while (true)
{
var entries = await db.StreamReadGroupAsync(channel, group, Environment.MachineName, count: 1);
foreach (var entry in entries)
{
_logger.LogInformation($"Consumer {Environment.MachineName} received: {channel} {entry.Values[0].Value}");
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}");
}
finally
{
await db.StreamAcknowledgeAsync(channel, group, entry.Id);
}
}
await Task.Delay(Random.Shared.Next(1, 11) * 100);
}
}
}

View file

@ -23,17 +23,23 @@ public static class Utilities
var data = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
var sb = new StringBuilder();
foreach(var c in data)
foreach (var c in data)
{
sb.Append(c.ToString("x2"));
}
return sb.ToString();
}
public static (string, string) SplitAsTuple(this string str, string sep)
public static (string, string, string) SplitAsTuple(this string str, string sep)
{
var splits = str.Split(sep);
return (splits[0], splits[1]);
if (splits.Length == 2 || string.IsNullOrWhiteSpace(splits[2]))
{
return (splits[0], splits[1], "CN");
}
return (splits[0], splits[1], splits[2]);
}
/// <summary>
@ -47,4 +53,36 @@ public static class Utilities
memcache.Compact(100);
}
}
public static string HideMiddleDigits(string input, bool isEmail = false)
{
if (string.IsNullOrWhiteSpace(input))
{
return input;
}
if (isEmail)
{
int atIndex = input.IndexOf('@');
if (atIndex > 1)
{
string localPart = input.Substring(0, atIndex);
if (localPart.Length > 2)
{
string maskedLocalPart = $"{localPart[0]}{new string('*', localPart.Length - 2)}{localPart[^1]}";
return $"{maskedLocalPart}@{input.Substring(atIndex + 1)}";
}
}
}
else
{
if (input.Length > 6)
{
return $"{input.Substring(0, 3)}{new string('*', input.Length - 6)}{input.Substring(input.Length - 3)}";
}
}
return input;
}
}

View file

@ -11,9 +11,21 @@ public partial class FileRepository
return Users.FirstOrDefault(x => x.Email == email.ToLower());
}
public User? GetUserByPhone(string phone)
public User? GetUserByPhone(string phone, string? type = UserType.Client, string regionCode = "CN")
{
return Users.FirstOrDefault(x => x.Phone == phone);
var query = Users.Where(x => x.Phone == phone);
if (!string.IsNullOrEmpty(type))
{
query = query.Where(x => x.Type == type);
}
if (!string.IsNullOrEmpty(regionCode))
{
query = query.Where(x => x.RegionCode == regionCode);
}
return query.FirstOrDefault();
}
public User? GetAffiliateUserByPhone(string phone)

View file

@ -148,7 +148,7 @@ public class UserService : IUserService
public async Task<Token?> GetAffiliateToken(string authorization)
{
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
var (id, password) = base64.SplitAsTuple(":");
var (id, password, regionCode) = base64.SplitAsTuple(":");
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAffiliateUserByPhone(id);
var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate;
@ -170,9 +170,9 @@ public class UserService : IUserService
public async Task<Token?> GetAdminToken(string authorization)
{
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
var (id, password) = base64.SplitAsTuple(":");
var (id, password, regionCode) = base64.SplitAsTuple(":");
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetUserByPhone(id,"admin");
var record = db.GetUserByPhone(id, type: UserType.Internal);
var isCanLogin = record != null && !record.IsDisabled
&& record.Type == UserType.Internal && new List<string>
{
@ -210,13 +210,13 @@ public class UserService : IUserService
public async Task<Token?> GetToken(string authorization)
{
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
var (id, password) = base64.SplitAsTuple(":");
var (id, password, regionCode) = base64.SplitAsTuple(":");
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
if (record == null)
{
record = db.GetUserByPhone(id);
record = db.GetUserByPhone(id, regionCode: regionCode);
}
if (record != null && record.Type == UserType.Affiliate)
@ -525,7 +525,7 @@ public class UserService : IUserService
public async Task<bool> VerifyEmailExisting(string email)
{
if (string.IsNullOrEmpty(email))
if (string.IsNullOrWhiteSpace(email))
{
return true;
}
@ -542,13 +542,13 @@ public class UserService : IUserService
public async Task<bool> VerifyPhoneExisting(string phone, string regionCode)
{
if (string.IsNullOrEmpty(phone))
if (string.IsNullOrWhiteSpace(phone))
{
return true;
}
var db = _services.GetRequiredService<IBotSharpRepository>();
var UserByphone = db.GetUserByPhone(phone, regionCode);
var UserByphone = db.GetUserByPhone(phone, regionCode: regionCode);
if (UserByphone != null && UserByphone.Verified)
{
return true;
@ -570,7 +570,7 @@ public class UserService : IUserService
if (!string.IsNullOrEmpty(user.Phone))
{
record = db.GetUserByPhone(user.Phone);
record = db.GetUserByPhone(user.Phone, regionCode: user.RegionCode);
}
if (!string.IsNullOrEmpty(user.Email))
@ -745,7 +745,7 @@ public class UserService : IUserService
await Task.CompletedTask;
return true;
}
public async Task<bool> RemoveDashboardConversation(string userId, string conversationId)
{
var db = _services.GetRequiredService<IBotSharpRepository>();

View file

@ -13,6 +13,7 @@ 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
@ -25,7 +26,8 @@ public class UserCreationModel
Password = Password,
Role = Role,
Type = Type,
RegionCode = RegionCode
RegionCode = RegionCode,
ReferralCode = ReferralCode
};
}
}

View file

@ -57,6 +57,8 @@ public class UserViewModel
UserName = user.UserName,
FirstName = user.FirstName,
LastName = user.LastName,
//Email = Utilities.HideMiddleDigits(user.Email, true),
//Phone = Utilities.HideMiddleDigits((!string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone)),
Email = user.Email,
Phone = !string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone,
Type = user.Type,

View file

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

View file

@ -51,6 +51,8 @@ public class UserDocument : MongoBase
Verified = Verified,
RegionCode = RegionCode,
Permissions = Permissions,
CreatedTime = CreatedTime,
UpdatedTime = UpdatedTime,
};
}
}

View file

@ -13,7 +13,7 @@ public partial class MongoRepository
return user != null ? user.ToUser() : null;
}
public User? GetUserByPhone(string phone, string role = null, string regionCode = "CN")
public User? GetUserByPhone(string phone, string type = UserType.Client, string regionCode = "CN")
{
string phoneSecond = string.Empty;
// if phone number length is less than 4, return null
@ -22,11 +22,18 @@ public partial class MongoRepository
return null;
}
phoneSecond = phone.StartsWith("+86") ? phone.Replace("+86", "") : $"+86{phone}";
if (regionCode == "CN")
{
phoneSecond = (phone ?? "").StartsWith("+86") ? (phone ?? "").Replace("+86", "") : ($"+86{phone ?? ""}");
}
else
{
phoneSecond = (phone ?? "").Substring(regionCode == "US" ? 2 : 3);
}
var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond) && x.Type != UserType.Affiliate
&& (x.RegionCode == regionCode || string.IsNullOrWhiteSpace(x.RegionCode))
&& (role == "admin" ? x.Role == "admin" || x.Role == "root" : true));
var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond)
&& (x.RegionCode == regionCode || string.IsNullOrWhiteSpace(x.RegionCode))
&& (x.Type == type));
return user != null ? user.ToUser() : null;
}
@ -245,7 +252,7 @@ public partial class MongoRepository
user.AgentActions = agentActions;
return user;
}
var agentIds = userAgents.Select(x => x.AgentId)?.Distinct().ToList();
if (!agentIds.IsNullOrEmpty())
{

View file

@ -79,10 +79,102 @@ public partial class PlaywrightWebDriver
{
await locator.HoverAsync();
}
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;
await mouse.MoveAsync(startX, startY);
await mouse.DownAsync();
// 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
await mouse.UpAsync();
}
else
{
throw new NotImplementedException();
}
}
}
if (action.WaitTime > 0)
{
await Task.Delay(1000 * action.WaitTime);
}
}
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;
}
}

View file

@ -8,7 +8,7 @@ public partial class PlaywrightWebDriver
var context = await _instance.GetContext(message.ContextId);
try
{
var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback,
var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
excludeResponseUrls: args.ExcludeResponseUrls,
@ -18,7 +18,7 @@ public partial class PlaywrightWebDriver
if (args.OpenNewTab && page != null && page.Url == "about:blank")
{
page = await _instance.NewPage(message,
page = await _instance.NewPage(message,
enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
@ -28,7 +28,7 @@ public partial class PlaywrightWebDriver
if (page == null)
{
page = await _instance.NewPage(message,
page = await _instance.NewPage(message,
enableResponseCallback: args.EnableResponseCallback,
responseInMemory: args.ResponseInMemory,
responseContainer: args.ResponseContainer,
@ -47,7 +47,7 @@ public partial class PlaywrightWebDriver
if (args.Selectors != null)
{
// 使用传入的选择器列表进行并行等待
var tasks =args.Selectors.Select(selector =>
var tasks = args.Selectors.Select(selector =>
page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions
{
Timeout = args.Timeout > 0 ? args.Timeout : 30000
@ -92,7 +92,7 @@ public partial class PlaywrightWebDriver
}
}
else
{
{
result.Message = response.StatusText;
}
}

View file

@ -5,7 +5,8 @@ public partial class PlaywrightWebDriver
public async Task<BrowserActionResult> ScrollPage(MessageInfo message, PageActionArgs args)
{
var result = new BrowserActionResult();
await _instance.Wait(message.ContextId);
var waitTime = args.WaitTime > 0 ? args.WaitTime : 10;
await _instance.Wait(message.ContextId, waitTime, args.WaitForNetworkIdle);
var page = _instance.GetPage(message.ContextId);
@ -31,7 +32,7 @@ public partial class PlaywrightWebDriver
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
// Scroll to the bottom
await page.Mouse.WheelAsync(0, -scrollY);
await page.Mouse.WheelAsync(0, scrollY);
}
else if (args.Direction == "top")
{