Files
wx-win-agent/tests/node-agent/WxAgent.Remote.Integration/Program.cs
T
rogee 13c31fc902
Build web service image / build (push) Successful in 1m53s
feat: add remote control plane and whitelist reads
2026-09-12 09:46:05 +08:00

145 lines
8.6 KiB
C#

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using WxAgent.Core;
var baseUrl = Required("--base-url");
var nodeId = Required("--node-id");
var nodeToken = Required("--node-token");
var webUser = Required("--web-user");
var webPassword = Required("--web-password");
var accountId = Get("--account-id") ?? "account-a";
var allowedChatId = Get("--allowed-chat-id") ?? "allowed-chat";
var blockedChatId = Get("--blocked-chat-id") ?? "blocked-chat";
using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
var webToken = await LoginAsync(http, webUser, webPassword);
var config = new ReportingConfig
{
Enabled = true,
ConfigVersion = 1,
Accounts = [new AccountReportingConfig
{
AccountId = accountId,
Enabled = true,
AllowedChats = [new AllowedChat { Type = ReportingChatType.Group, ChatId = allowedChatId, Enabled = true, IdentityVerified = true }]
}]
};
using var node = new RemoteControlClient(new RemoteAgentOptions
{
AuthAddress = baseUrl,
Token = nodeToken,
NodeId = nodeId,
ActiveAccountId = accountId
}, http);
await node.RegisterAsync(new RemoteNodeRegistration(nodeId, "integration", RemoteProtocol.Version,
["heartbeat", "poll-tasks", "send-text", "report-message"], config.ConfigVersion,
[new RemoteAccountSummary(accountId, true, true, 1, 0)]));
await node.HeartbeatAsync(new RemoteHeartbeat(nodeId, "integration", RemoteProtocol.Version, RemoteNodeStatus.Online,
true, true, false, accountId, 0, config.ConfigVersion, "integration-heartbeat"));
var payload = JsonDocument.Parse("{\"target_id\":\"target-chat\",\"text\":\"integration task\",\"confirmed\":true}").RootElement.Clone();
var taskResponse = await WebPostAsync<RemoteTaskSubmissionResponse>(http, "/v1/tasks", webToken, new RemoteTaskSubmission(
nodeId, accountId, "send-text", "integration-task-1", payload));
var task = (await node.PollTasksAsync(accountId)).Single(item => item.TaskId == taskResponse.TaskId);
var accepted = await node.AcknowledgeTaskAsync(task);
var started = await node.StartTaskAsync(accepted);
await node.SendTaskResultAsync(new RemoteTaskResult(started.TaskId, accountId, started.LeaseGeneration,
RemoteTaskStatus.Succeeded, null, null, true, null, "integration-result"), config);
var readResponse = await WebPostAsync<RemoteTaskSubmissionResponse>(http, "/v1/reads/sessions", webToken,
new { node_id = nodeId, account_id = accountId, idempotency_key = "integration-read-1", limit = 20, offset = 0 });
var readTask = (await node.PollTasksAsync(accountId)).Single(item => item.TaskId == readResponse.TaskId);
var readAccepted = await node.AcknowledgeTaskAsync(readTask);
var readStarted = await node.StartTaskAsync(readAccepted);
using var readContentDocument = JsonDocument.Parse("{\"items\":[{\"automationId\":\"allowed-chat\"}],\"limit\":20,\"offset\":0,\"hasMore\":false}");
await node.SendTaskResultAsync(new RemoteTaskResult(readStarted.TaskId, accountId, readStarted.LeaseGeneration,
RemoteTaskStatus.Succeeded, null, null, false, readContentDocument.RootElement.Clone(), "integration-read-result"), config,
chatScopes: [new RemoteReportingScope(allowedChatId, ReportingChatType.Group)]);
var storedRead = await WebGetAsync<JsonElement>(http, "/v1/tasks/" + readResponse.TaskId, webToken);
if (!storedRead.TryGetProperty("result", out var readResult)
|| !readResult.TryGetProperty("content", out var readContent)
|| readContent.ValueKind != JsonValueKind.Object)
throw new InvalidOperationException("Remote read result content was not retained.");
var denied = await node.SubmitEventAsync(config, new RemoteMessageEvent(nodeId, accountId, blockedChatId,
ReportingChatType.Private, 1, "message", DateTimeOffset.UtcNow, "blocked-content", config.ConfigVersion,
config.ConfigVersion, "integration-denied"));
var acceptedEvent = await node.SubmitEventAsync(config, new RemoteMessageEvent(nodeId, accountId, allowedChatId,
ReportingChatType.Group, 1, "message", DateTimeOffset.UtcNow, "allowed-content", config.ConfigVersion,
config.ConfigVersion, "integration-allowed"));
var events = await WebGetAsync<EventList>(http, "/v1/events?limit=20", webToken);
if (denied.Accepted || !acceptedEvent.Accepted || events.Events.Count != 1 || events.Events[0].ChatId != allowedChatId)
throw new InvalidOperationException("Remote whitelist integration assertion failed.");
Console.WriteLine(JsonSerializer.Serialize(new
{
status = "ok",
node = nodeId,
task = taskResponse.TaskId,
taskStatus = RemoteTaskStatus.Succeeded.ToString(),
readTask = readResponse.TaskId,
readContentRetained = true,
deniedEventAccepted = denied.Accepted,
acceptedEvent = acceptedEvent.EventId,
storedEventCount = events.Events.Count,
privacy = "blocked event content was not sent"
}, new JsonSerializerOptions { WriteIndented = true }));
async Task<T> WebPostAsync<T>(HttpClient client, string path, string token, object value)
{
using var request = new HttpRequestMessage(HttpMethod.Post, path);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent(JsonSerializer.Serialize(value, RemoteJson.Options), Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {content}");
return JsonSerializer.Deserialize<T>(content, RemoteJson.Options) ?? throw new InvalidOperationException("Empty response.");
}
async Task<T> WebGetAsync<T>(HttpClient client, string path, string token)
{
using var request = new HttpRequestMessage(HttpMethod.Get, path);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {content}");
return JsonSerializer.Deserialize<T>(content, RemoteJson.Options) ?? throw new InvalidOperationException("Empty response.");
}
async Task<string> LoginAsync(HttpClient client, string username, string password)
{
var result = await WebPostAsync<LoginResponse>(client, "/v1/auth/login", "", new { username, password });
return result.AccessToken;
}
string Required(string name) => Get(name) ?? throw new ArgumentException($"Missing {name}.");
string? Get(string name)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : null;
}
sealed record LoginResponse(
[property: System.Text.Json.Serialization.JsonPropertyName("access_token")] string AccessToken,
[property: System.Text.Json.Serialization.JsonPropertyName("token_type")] string TokenType,
[property: System.Text.Json.Serialization.JsonPropertyName("expires_in")] int ExpiresIn,
[property: System.Text.Json.Serialization.JsonPropertyName("correlation_id")] string CorrelationId);
sealed record EventList([property: System.Text.Json.Serialization.JsonPropertyName("events")] IReadOnlyList<EventView> Events);
sealed record EventView(
[property: System.Text.Json.Serialization.JsonPropertyName("event_id")] string EventId,
[property: System.Text.Json.Serialization.JsonPropertyName("node_id")] string NodeId,
[property: System.Text.Json.Serialization.JsonPropertyName("account_id")] string AccountId,
[property: System.Text.Json.Serialization.JsonPropertyName("chat_id")] string ChatId,
[property: System.Text.Json.Serialization.JsonPropertyName("chat_type")] string ChatType,
[property: System.Text.Json.Serialization.JsonPropertyName("event_seq")] long EventSeq,
[property: System.Text.Json.Serialization.JsonPropertyName("event_type")] string EventType,
[property: System.Text.Json.Serialization.JsonPropertyName("occurred_at")] DateTimeOffset OccurredAt,
[property: System.Text.Json.Serialization.JsonPropertyName("content")] string? Content,
[property: System.Text.Json.Serialization.JsonPropertyName("config_version")] long ConfigVersion,
[property: System.Text.Json.Serialization.JsonPropertyName("authorization_version")] long AuthorizationVersion,
[property: System.Text.Json.Serialization.JsonPropertyName("correlation_id")] string CorrelationId,
[property: System.Text.Json.Serialization.JsonPropertyName("authorized")] bool Authorized,
[property: System.Text.Json.Serialization.JsonPropertyName("content_hash")] string ContentHash,
[property: System.Text.Json.Serialization.JsonPropertyName("received_at")] DateTimeOffset ReceivedAt);