feat: validate single-client broadcast operations
Build web service image / build (push) Successful in 1m9s

This commit is contained in:
2026-09-19 14:33:26 +08:00
parent e321d38fa3
commit c7c0ab273f
62 changed files with 2861 additions and 12651 deletions
+12 -7
View File
@@ -4,7 +4,7 @@ namespace WxAgent.Service;
public sealed record OperationRecord(string Id, string PrincipalId, string AccountId, string Capability,
string State, string Stage, string CorrelationId, DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt,
string? ErrorCode, bool HasSideEffects);
string? ErrorCode, bool HasSideEffects, string? Details = null);
public sealed record OperationSummary(string Id, string AccountId, string Capability,
string State, string Stage, string CorrelationId, DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt,
@@ -37,10 +37,12 @@ public sealed class OperationStore : IDisposable
stage='restart', error='AgentRestarted' WHERE state='Running';
UPDATE operations SET state='Cancelled', stage='restart', error='AgentRestarted' WHERE state='Queued';
""");
try { Execute("ALTER TABLE operations ADD COLUMN details TEXT"); }
catch (SqliteException exception) when (exception.SqliteErrorCode == 1) { }
}
public (OperationRecord Record, bool Created) Enqueue(string principal, string account, string capability,
string? idempotency, string digest, bool sideEffects, TimeSpan budget, int capacity)
string? idempotency, string digest, bool sideEffects, TimeSpan budget, int capacity, string? details = null)
{
lock (gate)
{
@@ -67,10 +69,10 @@ public sealed class OperationStore : IDisposable
throw new ServiceException("QueueFull", 429, "Agent queue is full.");
var now = DateTimeOffset.UtcNow;
var operation = new OperationRecord(Guid.NewGuid().ToString("N"), principal, account, capability,
"Queued", "queued", Guid.NewGuid().ToString("N"), now, now.Add(budget), null, sideEffects);
"Queued", "queued", Guid.NewGuid().ToString("N"), now, now.Add(budget), null, sideEffects, details);
using var insert = database.CreateCommand();
insert.Transaction = transaction;
insert.CommandText = "INSERT INTO operations VALUES ($id,$p,$a,$c,'Queued','queued',$correlation,$created,$expires,NULL,$effects,$key,$digest)";
insert.CommandText = "INSERT INTO operations (id,principal,account,capability,state,stage,correlation,created,expires,error,side_effects,idempotency,digest,details) VALUES ($id,$p,$a,$c,'Queued','queued',$correlation,$created,$expires,NULL,$effects,$key,$digest,$details)";
insert.Parameters.AddWithValue("$id", operation.Id);
insert.Parameters.AddWithValue("$p", principal);
insert.Parameters.AddWithValue("$a", account);
@@ -81,6 +83,7 @@ public sealed class OperationStore : IDisposable
insert.Parameters.AddWithValue("$effects", sideEffects ? 1 : 0);
insert.Parameters.AddWithValue("$key", (object?)idempotency ?? DBNull.Value);
insert.Parameters.AddWithValue("$digest", digest);
insert.Parameters.AddWithValue("$details", (object?)details ?? DBNull.Value);
insert.ExecuteNonQuery();
transaction.Commit();
return (operation, true);
@@ -122,16 +125,17 @@ public sealed class OperationStore : IDisposable
}
}
public void Transition(string id, string state, string stage, string? error = null)
public void Transition(string id, string state, string stage, string? error = null, string? details = null)
{
lock (gate)
{
using var command = database.CreateCommand();
command.CommandText = "UPDATE operations SET state=$state,stage=$stage,error=$error WHERE id=$id AND state IN ('Queued','Running')";
command.CommandText = "UPDATE operations SET state=$state,stage=$stage,error=$error,details=COALESCE($details,details) WHERE id=$id AND state IN ('Queued','Running')";
command.Parameters.AddWithValue("$id", id);
command.Parameters.AddWithValue("$state", state);
command.Parameters.AddWithValue("$stage", stage);
command.Parameters.AddWithValue("$error", (object?)error ?? DBNull.Value);
command.Parameters.AddWithValue("$details", (object?)details ?? DBNull.Value);
command.ExecuteNonQuery();
}
}
@@ -139,7 +143,8 @@ public sealed class OperationStore : IDisposable
private static OperationRecord Read(SqliteDataReader reader) => new(reader.GetString(0), reader.GetString(1), reader.GetString(2),
reader.GetString(3), reader.GetString(4), reader.GetString(5), reader.GetString(6),
DateTimeOffset.Parse(reader.GetString(7), System.Globalization.CultureInfo.InvariantCulture),
DateTimeOffset.Parse(reader.GetString(8), System.Globalization.CultureInfo.InvariantCulture), reader.IsDBNull(9) ? null : reader.GetString(9), reader.GetInt64(10) != 0);
DateTimeOffset.Parse(reader.GetString(8), System.Globalization.CultureInfo.InvariantCulture), reader.IsDBNull(9) ? null : reader.GetString(9), reader.GetInt64(10) != 0,
reader.IsDBNull(13) ? null : reader.GetString(13));
private void Execute(string sql)
{