diff --git a/docs/validation/Review-Database-Management-Safety.md b/docs/validation/Review-Database-Management-Safety.md index 649876e..f021d8b 100644 --- a/docs/validation/Review-Database-Management-Safety.md +++ b/docs/validation/Review-Database-Management-Safety.md @@ -14,9 +14,9 @@ - `db contacts` 与管理 API 共用读取器;分页 SQL 使用参数绑定、稳定 username 排序及多取一行判断 `hasMore`,不以显示名去重。 - CLI 新增 `--limit`(1–10000,默认 200)、`--offset`(默认 0),返回 `hasMore/nextOffset`。`--contains` 是区分大小写的字面量子串;默认输出仍脱敏。 - C# 入口 `GetContactsPageAsync` 支持账号、密钥文件和群/非群筛选。未选择账号且密钥缓存包含多个账号时返回 `InvalidArgument`,不会随便选取一个。 -- `GetFriendsAsync` 保留显式数量上限;全量读取用分页接口。`GetRecentGroupsAsync` 兼容名称仅返回数据库已知群,不承诺最近活动顺序。 +- `GetFriendsAsync` 保留显式数量上限;全量读取用分页接口。群列表使用 `GetContactsPageAsync` 分页读取,不承诺最近活动顺序。 - 联系人详情只映射已知字段;未知标签、签名、来源及共同群数量保持 null。非群联系人数据不能用于证明好友关系。 -- 群成员使用 `chat_room`、`chatroom_member`、`contact` 的只读 join,`GetGroupMembersAsync` 支持稳定 username 和分页,不回退 UI 枚举;已用测试群脱敏验证。 +- 群成员使用 `chat_room`、`chatroom_member`、`contact` 的只读 join,`GetGroupMembersPageAsync` 支持稳定 username 和分页,不回退 UI 枚举;已用测试群脱敏验证。 - offset 分页不是跨页事务快照;期间发生增删时仍可能重复/遗漏。 ### URL 卡片与草稿 diff --git a/node-agent/WxAgent.Core/RemoteNodeConfiguration.cs b/node-agent/WxAgent.Core/RemoteNodeConfiguration.cs index 8ba75a7..a7a6624 100644 --- a/node-agent/WxAgent.Core/RemoteNodeConfiguration.cs +++ b/node-agent/WxAgent.Core/RemoteNodeConfiguration.cs @@ -37,26 +37,10 @@ public sealed record RemoteNodeConfiguration Audit = (Audit ?? []).TakeLast(1000).ToArray() }; } - - public RemoteNodeConfiguration WithAudit(string action) - { - RemoteAgentOptions.ValidateIdentifier(action, "action", 120); - return this with - { - Audit = (Audit ?? []).Append(new RemoteConfigurationAuditEntry - { - At = DateTimeOffset.UtcNow, - Action = action, - ConfigVersion = Reporting?.ConfigVersion ?? 0 - }).TakeLast(1000).ToArray() - }; - } } public static class RemoteNodeConfigurationStore { - private static readonly SemaphoreSlim Gate = new(1, 1); - public static async Task LoadAsync(string path, CancellationToken cancellationToken = default) { try @@ -82,40 +66,4 @@ public static class RemoteNodeConfigurationStore throw; } } - - public static async Task SaveAsync(string path, RemoteNodeConfiguration configuration, CancellationToken cancellationToken = default) - { - var normalized = configuration.NormalizeAndValidate(); - var destination = Path.GetFullPath(path); - var directory = Path.GetDirectoryName(destination) ?? AppContext.BaseDirectory; - Directory.CreateDirectory(directory); - await Gate.WaitAsync(cancellationToken); - try - { - var temporary = destination + ".tmp-" + Guid.NewGuid().ToString("N"); - try - { - await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) - { - await JsonSerializer.SerializeAsync(stream, normalized, RemoteJson.Options, cancellationToken); - await stream.FlushAsync(cancellationToken); - } - if (!OperatingSystem.IsWindows()) - { - try { File.SetUnixFileMode(temporary, UnixFileMode.UserRead | UnixFileMode.UserWrite); } - catch (PlatformNotSupportedException) { } - } - if (OperatingSystem.IsWindows() && File.Exists(destination)) File.Replace(temporary, destination, null); - else File.Move(temporary, destination, true); - } - finally - { - if (File.Exists(temporary)) File.Delete(temporary); - } - } - finally - { - Gate.Release(); - } - } } diff --git a/node-agent/WxAgent.Host/Program.cs b/node-agent/WxAgent.Host/Program.cs index dce1816..e9825c8 100644 --- a/node-agent/WxAgent.Host/Program.cs +++ b/node-agent/WxAgent.Host/Program.cs @@ -216,7 +216,7 @@ try await WechatChatClient.CloseSubWindowAsync("文件传输助手", cancellationToken); var contacts = await WechatChatClient.GetFriendsAsync(5000, cancellationToken); var newFriends = await WechatChatClient.GetNewFriendsAsync(true, cancellationToken); - var groups = await WechatChatClient.GetRecentGroupsAsync(cancellationToken); + var groups = await WechatChatClient.GetContactsPageAsync(limit: 200, groupsOnly: true, cancellationToken: cancellationToken); var myInfo = await WechatChatClient.GetMyInfoAsync(cancellationToken); await WechatChatClient.SwitchToChatsAsync(cancellationToken); var confirmationGuarded = false; @@ -236,7 +236,7 @@ try historyCount = history.Count, contactCount = contacts.Count, newFriendCount = newFriends.Count, - recentGroupCount = groups.Count, + groupPageCount = groups.Contacts.Count, accountDetected = !string.IsNullOrWhiteSpace(myInfo.DisplayName), confirmationGuarded, privacy = "names and account identifiers omitted" diff --git a/node-agent/WxAgent.Host/RemoteCliCommands.cs b/node-agent/WxAgent.Host/RemoteCliCommands.cs index 71a46d2..aaf6f3c 100644 --- a/node-agent/WxAgent.Host/RemoteCliCommands.cs +++ b/node-agent/WxAgent.Host/RemoteCliCommands.cs @@ -1,4 +1,3 @@ -using System.Net.Http; using WxAgent.Core; namespace WxAgent.Host; @@ -17,19 +16,10 @@ internal static class RemoteCliCommands return (group, action) switch { ("auth", "show") => await AuthShowAsync(configPath, cancellationToken), - ("auth", "set") => await AuthSetAsync(args, configPath, cancellationToken), - ("auth", "clear") => await AuthClearAsync(configPath, cancellationToken), ("probe", "run") => await ProbeAsync(configPath, cancellationToken), ("status", "show") => await StatusShowAsync(args, configPath, cancellationToken), ("reporting", "show") => await ReportingShowAsync(configPath, cancellationToken), - ("reporting", "enable") => await ReportingToggleAsync(configPath, true, cancellationToken), - ("reporting", "disable") => await ReportingToggleAsync(configPath, false, cancellationToken), - ("reporting", "account-add") => await AccountToggleAsync(args, configPath, false, cancellationToken), - ("reporting", "account-enable") => await AccountToggleAsync(args, configPath, true, cancellationToken), - ("reporting", "account-disable") => await AccountToggleAsync(args, configPath, false, cancellationToken), - ("reporting", "allow") => await AllowChatAsync(args, configPath, cancellationToken), - ("reporting", "deny") => await DenyChatAsync(args, configPath, cancellationToken), - _ => throw Invalid("Unknown remote command. Use: auth show|set|clear, status show, probe run, reporting show|enable|disable|account-add|account-enable|account-disable|allow|deny.") + _ => throw Invalid("Unknown remote command. Use: auth show, status show, probe run, reporting show.") }; } @@ -45,34 +35,6 @@ internal static class RemoteCliCommands }; } - private static async Task AuthSetAsync(string[] args, string path, CancellationToken cancellationToken) - { - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - var remote = new RemoteAgentOptions - { - AuthAddress = Required(args, "--address"), - Token = Option(args, "--token"), - TokenFile = Option(args, "--token-file"), - ServerCaFile = Option(args, "--server-ca-file"), - ClientCertificateFile = Option(args, "--client-certificate-file"), - ClientCertificateKeyFile = Option(args, "--client-certificate-key-file"), - NodeId = Required(args, "--node"), - ActiveAccountId = Option(args, "--active-account"), - AllowInsecureHttp = Has(args, "--allow-insecure-http") - }; - remote.Validate(); - var next = (current with { Remote = remote }).WithAudit("remote.auth.set"); - await RemoteNodeConfigurationStore.SaveAsync(path, next, cancellationToken); - return new { saved = path, remote = remote.Redacted() }; - } - - private static async Task AuthClearAsync(string path, CancellationToken cancellationToken) - { - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - await RemoteNodeConfigurationStore.SaveAsync(path, (current with { Remote = new RemoteAgentOptions() }).WithAudit("remote.auth.clear"), cancellationToken); - return new { saved = path, configured = false }; - } - private static async Task ProbeAsync(string path, CancellationToken cancellationToken) { var configuration = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); @@ -129,72 +91,6 @@ internal static class RemoteCliCommands }; } - private static async Task ReportingToggleAsync(string path, bool enabled, CancellationToken cancellationToken) - { - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - var reporting = ReportingConfigStore.Update(current.Reporting, value => value with { Enabled = enabled }); - await RemoteNodeConfigurationStore.SaveAsync(path, (current with { Reporting = reporting }).WithAudit(enabled ? "reporting.enable" : "reporting.disable"), cancellationToken); - return new { saved = path, reporting.Enabled, reporting.ConfigVersion }; - } - - private static async Task AccountToggleAsync(string[] args, string path, bool enabled, CancellationToken cancellationToken) - { - var accountId = Required(args, "--account"); - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - var accounts = current.Reporting.Accounts.ToList(); - var index = accounts.FindIndex(account => string.Equals(account.AccountId, accountId, StringComparison.Ordinal)); - if (index < 0) - { - if (args[2] != "account-add") throw Invalid("The account does not exist; run reporting account-add first."); - accounts.Add(new AccountReportingConfig { AccountId = accountId, Enabled = enabled }); - } - else - { - accounts[index] = accounts[index] with { Enabled = enabled }; - } - var reporting = ReportingConfigStore.Update(current.Reporting, value => value with { Accounts = accounts }); - await RemoteNodeConfigurationStore.SaveAsync(path, (current with { Reporting = reporting }).WithAudit($"reporting.account.{(enabled ? "enable" : "disable")}"), cancellationToken); - return new { saved = path, accountId = Mask(accountId), enabled, reporting.ConfigVersion }; - } - - private static async Task AllowChatAsync(string[] args, string path, CancellationToken cancellationToken) - { - if (!Has(args, "--identity-verified")) - throw Invalid("Allowing a chat requires --identity-verified after the stable identity was confirmed."); - var accountId = Required(args, "--account"); - var chatId = Required(args, "--chat-id"); - var type = ParseChatType(Required(args, "--type")); - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - var accounts = current.Reporting.Accounts.ToList(); - var index = accounts.FindIndex(account => string.Equals(account.AccountId, accountId, StringComparison.Ordinal)); - if (index < 0 || !accounts[index].Enabled) - throw Invalid("The account must exist and be enabled before a chat can be allowed."); - var chats = accounts[index].AllowedChats.Where(chat => chat.Type != type || chat.ChatId != chatId).ToList(); - chats.Add(new AllowedChat { Type = type, ChatId = chatId, Enabled = true, IdentityVerified = true }); - accounts[index] = accounts[index] with { AllowedChats = chats }; - var reporting = ReportingConfigStore.Update(current.Reporting, value => value with { Accounts = accounts }); - await RemoteNodeConfigurationStore.SaveAsync(path, (current with { Reporting = reporting }).WithAudit("reporting.chat.allow"), cancellationToken); - return new { saved = path, accountId = Mask(accountId), chatId = Mask(chatId), type, reporting.ConfigVersion }; - } - - private static async Task DenyChatAsync(string[] args, string path, CancellationToken cancellationToken) - { - var accountId = Required(args, "--account"); - var chatId = Required(args, "--chat-id"); - var type = ParseChatType(Required(args, "--type")); - var current = await RemoteNodeConfigurationStore.LoadAsync(path, cancellationToken); - var accounts = current.Reporting.Accounts.ToList(); - var index = accounts.FindIndex(account => string.Equals(account.AccountId, accountId, StringComparison.Ordinal)); - if (index < 0) throw Invalid("The account does not exist."); - accounts[index] = accounts[index] with - { - AllowedChats = accounts[index].AllowedChats.Where(chat => chat.Type != type || chat.ChatId != chatId).ToArray() - }; - var reporting = ReportingConfigStore.Update(current.Reporting, value => value with { Accounts = accounts }); - await RemoteNodeConfigurationStore.SaveAsync(path, (current with { Reporting = reporting }).WithAudit("reporting.chat.deny"), cancellationToken); - return new { saved = path, accountId = Mask(accountId), chatId = Mask(chatId), type, reporting.ConfigVersion }; - } - private static bool IsConfigurationCommand(string[] args) => (args[1], args[2]) switch { ("auth", "set" or "clear") => true, @@ -202,13 +98,6 @@ internal static class RemoteCliCommands _ => false }; - private static ReportingChatType ParseChatType(string value) => value.ToLowerInvariant() switch - { - "group" => ReportingChatType.Group, - "private" => ReportingChatType.Private, - _ => throw Invalid("--type must be group or private.") - }; - private static string Required(string[] args, string name) => Option(args, name) switch { { Length: > 0 } value => value, diff --git a/node-agent/WxAgent.Service/EventPump.cs b/node-agent/WxAgent.Service/EventPump.cs index ab0cc9f..3337e95 100644 --- a/node-agent/WxAgent.Service/EventPump.cs +++ b/node-agent/WxAgent.Service/EventPump.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Microsoft.Extensions.Hosting; using WxAgent.Core; @@ -20,11 +19,10 @@ public sealed class EventPump(IAgentBackend backend, EventHub hub, ServiceOption { await foreach (var item in source.ListenAsync(stoppingToken)) { - var runtime = await LoadRuntimeConfigurationAsync(options, stoppingToken); - if (remoteQueue is not null && runtime.Remote.IsConfigured && item.Kind == "message" + if (remoteQueue is not null && options.Remote is { IsConfigured: true } remote && item.Kind == "message" && item.ChatId is { Length: > 0 } chatId && item.ChatType is { } chatType) { - _ = remoteQueue.Enqueue(runtime.Reporting, runtime.Remote.NodeId!, item.AccountId, chatId, chatType, + _ = remoteQueue.Enqueue(options.Reporting, remote.NodeId!, item.AccountId, chatId, chatType, item.Kind, item.At, item.Content); } var localItem = item with { Content = null }; @@ -37,18 +35,4 @@ public sealed class EventPump(IAgentBackend backend, EventHub hub, ServiceOption catch (Exception) { await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } - - private static async Task LoadRuntimeConfigurationAsync(ServiceOptions options, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) - return new RemoteNodeConfiguration { Remote = options.Remote ?? new RemoteAgentOptions(), Reporting = options.Reporting }; - try - { - return await RemoteNodeConfigurationStore.LoadAsync(options.RemoteConfigurationFile, cancellationToken); - } - catch (Exception exception) when (exception is IOException or JsonException or WxAgentException) - { - return new RemoteNodeConfiguration(); - } - } } diff --git a/node-agent/WxAgent.Service/RemoteAgentHostedService.cs b/node-agent/WxAgent.Service/RemoteAgentHostedService.cs index 43d0290..2a27a10 100644 --- a/node-agent/WxAgent.Service/RemoteAgentHostedService.cs +++ b/node-agent/WxAgent.Service/RemoteAgentHostedService.cs @@ -19,31 +19,17 @@ public sealed class RemoteAgentHostedService( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - RemoteAgentOptions? configuredRemote = options.Remote; - if (!string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) - { - try - { - configuredRemote = (await RemoteNodeConfigurationStore.LoadAsync(options.RemoteConfigurationFile, stoppingToken)).Remote; - } - catch (Exception exception) when (exception is IOException or JsonException or WxAgentException) - { - logger.LogWarning("Remote agent is disabled because the external configuration could not be loaded; type={ExceptionType}.", exception.GetType().Name); - return; - } - } - if (configuredRemote is null || !configuredRemote.IsConfigured) + var remote = options.Remote; + if (remote is null || !remote.IsConfigured) { logger.LogInformation("Remote agent is disabled because authAddress, token or nodeId is not configured."); return; } - var remote = configuredRemote; using var client = new RemoteControlClient(remote); var ledger = new RemoteTaskLedger(Path.Combine(options.DataDirectory, "remote-task-ledger.json")); var eventQueue = remoteQueue ?? new RemoteEventQueue(Path.Combine(options.DataDirectory, "remote-event-queue.json")); // A configured Agent connection is the data-sync authorization boundary. - // EnableDataSync remains only as a read-compatibility property for old service.json files. var dataQueue = new RemoteDataBatchQueue( Path.Combine(options.DataDirectory, "remote-data-queue.json"), options.DataSyncQueueMaxItems, options.DataSyncQueueMaxBytes); var syncState = new RemoteDataSyncStateStore(Path.Combine(options.DataDirectory, "remote-data-sync-state.json")); @@ -52,33 +38,14 @@ public sealed class RemoteAgentHostedService( string? registeredActiveAccountId = null; bool? registeredActiveAccountVerified = null; long registeredReportingConfigVersion = -1; - var initialReporting = options.Reporting; - if (!string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) - { - try - { - initialReporting = (await LoadRuntimeConfigurationAsync(options, stoppingToken)).Reporting; - } - catch (Exception exception) when (exception is IOException or JsonException or WxAgentException) - { - logger.LogWarning("Remote reporting configuration could not be loaded for offline collection; type={ExceptionType}.", exception.GetType().Name); - } - } - ReportingConfig? activeReporting = ConnectionAuthorizedReporting(initialReporting, null); + ReportingConfig? activeReporting = ConnectionAuthorizedReporting(options.Reporting, null); var retry = RetryDelay; while (!stoppingToken.IsCancellationRequested) { try { - var runtime = await LoadRuntimeConfigurationAsync(options, stoppingToken); - if (!runtime.Remote.IsConfigured || !runtime.Remote.Equals(remote)) - { - logger.LogWarning("Remote configuration changed or is not valid; remote work is paused until the service is restarted."); - await DelayAsync(RetryDelay, stoppingToken); - continue; - } var snapshot = await ReadSnapshotAsync(remote, stoppingToken); - var reporting = ConnectionAuthorizedReporting(runtime.Reporting, snapshot); + var reporting = ConnectionAuthorizedReporting(options.Reporting, snapshot); activeReporting = reporting; var registrationNeedsRefresh = client.AuthState != RemoteAuthState.Authenticated || !string.Equals(registeredActiveAccountId, snapshot.ActiveAccountId, StringComparison.Ordinal) @@ -293,11 +260,6 @@ public sealed class RemoteAgentHostedService( { remote = await client.GetDataSyncStatusAsync(account.AccountId, cancellationToken, streamKey).ConfigureAwait(false); } - catch (RemoteClientException exception) when (exception.StatusCode == 404 && exception.Code == "NotFound") - { - // Older control planes do not expose the reconciliation endpoint; retain the legacy ACK path. - continue; - } catch (RemoteClientException exception) when (exception.StatusCode == 403 && exception.Code == "AccountNotAuthorized") { var droppedOnRevoke = queue.DropAllForAccount(account.AccountId); @@ -879,13 +841,6 @@ public sealed class RemoteAgentHostedService( JsonElement Content, IReadOnlyList ChatScopes); - private static async Task LoadRuntimeConfigurationAsync(ServiceOptions options, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) - return new RemoteNodeConfiguration { Remote = options.Remote ?? new RemoteAgentOptions(), Reporting = options.Reporting }; - return await RemoteNodeConfigurationStore.LoadAsync(options.RemoteConfigurationFile, cancellationToken); - } - private static async Task ReplayUnreportedResultsAsync( RemoteControlClient client, RemoteTaskLedger ledger, diff --git a/node-agent/WxAgent.Service/ServiceHost.cs b/node-agent/WxAgent.Service/ServiceHost.cs index db5743c..0843b77 100644 --- a/node-agent/WxAgent.Service/ServiceHost.cs +++ b/node-agent/WxAgent.Service/ServiceHost.cs @@ -54,10 +54,10 @@ public static class ServiceHost builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(p => p.GetRequiredService()); - if (options.Remote is not null || !string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) + if (options.Remote is not null) builder.Services.AddSingleton(new RemoteEventQueue(Path.Combine(options.DataDirectory, "remote-event-queue.json"))); builder.Services.AddHostedService(); - if (options.Remote is not null || !string.IsNullOrWhiteSpace(options.RemoteConfigurationFile)) + if (options.Remote is not null) builder.Services.AddHostedService(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/node-agent/WxAgent.Service/ServiceOptions.cs b/node-agent/WxAgent.Service/ServiceOptions.cs index bf9f916..7f827c6 100644 --- a/node-agent/WxAgent.Service/ServiceOptions.cs +++ b/node-agent/WxAgent.Service/ServiceOptions.cs @@ -1,7 +1,6 @@ using System.Net; using System.Security.Cryptography; using System.Text; -using System.Text.Json.Serialization; using WxAgent.Core; namespace WxAgent.Service; @@ -21,27 +20,16 @@ public sealed class ServiceOptions public required string DataDirectory { get; init; } public RemoteAgentOptions? Remote { get; init; } public ReportingConfig Reporting { get; init; } = new(); - public string? RemoteConfigurationFile { get; init; } // Connection authorization covers data synchronization; validation writes remain separately gated. public bool EnableValidationOperations { get; init; } public bool EnableListenerEvents { get; init; } public bool PreventAutoLock { get; init; } - // Kept as a compatibility switch for older service.json files; a configured Agent connection authorizes sync. - public bool EnableDataSync { get; init; } = true; public int DataSyncIntervalSeconds { get; init; } = 5; public int DataSyncBatchLimit { get; init; } = 100; public int DataSyncOverlapRows { get; init; } = 1; public int DataSyncQueueMaxItems { get; init; } = 1000; public long DataSyncQueueMaxBytes { get; init; } = 16 * 1024 * 1024; - // Kept only so older service.json files can be loaded and rewritten by the tray. - [JsonIgnore] - [Obsolete("Internal compatibility field; the value is ignored.")] - public int QueueCapacity { get; init; } = 100; - [JsonIgnore] - [Obsolete("Internal compatibility field; the value is ignored.")] - public string ListenerSession { get; init; } = "文件传输助手"; - public void Validate() { if (!Uri.TryCreate(ListenUrl, UriKind.Absolute, out var uri) || uri.Scheme != "http" || diff --git a/node-agent/WxAgent.Tray/Program.cs b/node-agent/WxAgent.Tray/Program.cs index 05d82b2..e148430 100644 --- a/node-agent/WxAgent.Tray/Program.cs +++ b/node-agent/WxAgent.Tray/Program.cs @@ -172,11 +172,9 @@ internal sealed class TrayApplicationContext : ApplicationContext DataDirectory = dataDirectory, Remote = source.Remote, Reporting = source.Reporting, - RemoteConfigurationFile = source.RemoteConfigurationFile, EnableValidationOperations = source.EnableValidationOperations, EnableListenerEvents = source.EnableListenerEvents, PreventAutoLock = source.PreventAutoLock, - EnableDataSync = source.EnableDataSync, DataSyncIntervalSeconds = source.DataSyncIntervalSeconds, DataSyncBatchLimit = source.DataSyncBatchLimit, DataSyncOverlapRows = source.DataSyncOverlapRows, @@ -288,12 +286,6 @@ internal sealed class TrayApplicationContext : ApplicationContext { EnsureConfiguration(); var settings = options!; - if (!string.IsNullOrWhiteSpace(settings.RemoteConfigurationFile)) - { - var remotePath = ResolvePath(settings.RemoteConfigurationFile, Path.GetDirectoryName(configPath)!, "remote.json"); - var remoteConfiguration = RemoteNodeConfigurationStore.LoadAsync(remotePath).GetAwaiter().GetResult(); - settings = WithRemoteForSettings(settings, remoteConfiguration.Remote, remoteConfiguration.Reporting); - } if (ServiceSettingsEditor.Show(settings) is not { } edited) return; WriteCredentials(edited.CredentialFile, edited.AccessToken!, edited.EnableValidationOperations); WriteJson(configPath, edited); @@ -306,29 +298,6 @@ internal sealed class TrayApplicationContext : ApplicationContext } } - private static ServiceOptions WithRemoteForSettings(ServiceOptions source, RemoteAgentOptions? remote, ReportingConfig reporting) => new() - { - ListenUrl = source.ListenUrl, - AllowExternal = source.AllowExternal, - AllowedHosts = source.AllowedHosts ?? [], - AllowedOrigins = source.AllowedOrigins ?? [], - AccessToken = source.AccessToken, - CredentialFile = source.CredentialFile, - DataDirectory = source.DataDirectory, - Remote = remote, - Reporting = reporting, - RemoteConfigurationFile = null, - EnableValidationOperations = source.EnableValidationOperations, - EnableListenerEvents = source.EnableListenerEvents, - PreventAutoLock = source.PreventAutoLock, - EnableDataSync = source.EnableDataSync, - DataSyncIntervalSeconds = source.DataSyncIntervalSeconds, - DataSyncBatchLimit = source.DataSyncBatchLimit, - DataSyncOverlapRows = source.DataSyncOverlapRows, - DataSyncQueueMaxItems = source.DataSyncQueueMaxItems, - DataSyncQueueMaxBytes = source.DataSyncQueueMaxBytes - }; - private void SetStatus(string message) { StatusItem.Text = message.Length > 60 ? message[..60] : message; @@ -618,7 +587,6 @@ internal static class ServiceSettingsEditor DataDirectory = current.DataDirectory, Remote = remoteOptions, Reporting = reportingOptions, - RemoteConfigurationFile = null, EnableValidationOperations = validation.Checked, EnableListenerEvents = listenerEvents.Checked, PreventAutoLock = preventAutoLock.Checked diff --git a/node-agent/WxAgent.Windows/WechatChatClient.Management.cs b/node-agent/WxAgent.Windows/WechatChatClient.Management.cs index ec5464f..4d17d69 100644 --- a/node-agent/WxAgent.Windows/WechatChatClient.Management.cs +++ b/node-agent/WxAgent.Windows/WechatChatClient.Management.cs @@ -79,15 +79,6 @@ public static partial class WechatChatClient item.DoubleClick(); } - /// Compatibility name: returns database-known groups, not a recency-ordered session list. - public static async Task> GetRecentGroupsAsync(CancellationToken cancellationToken = default) - { - var page = await GetContactsPageAsync(limit: 10000, groupsOnly: true, cancellationToken: cancellationToken).ConfigureAwait(false); - if (page.HasMore) - throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Group list exceeds the compatibility API limit; use GetContactsPageAsync to paginate."); - return page.Contacts.Select(contact => contact.DisplayName).ToArray(); - } - public static async Task GetContactsPageAsync(int limit = 200, int offset = 0, string? contains = null, bool? groupsOnly = null, string? accountId = null, string? keyFile = null, CancellationToken cancellationToken = default) @@ -265,14 +256,6 @@ public static partial class WechatChatClient public static Task SetMyNicknameInGroupAsync(string group, string value, string confirmation, CancellationToken cancellationToken = default) => SetGroupTextAsync(group, new[] { "我在本群的昵称", "群昵称" }, value, "set group nickname", confirmation, cancellationToken); - public static async Task> GetGroupMembersAsync(string group, CancellationToken cancellationToken = default) - { - var page = await GetGroupMembersPageAsync(group, 10000, cancellationToken: cancellationToken).ConfigureAwait(false); - if (page.HasMore) - throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Group member list exceeds the compatibility API limit; use GetGroupMembersPageAsync."); - return page.Members.Select(member => new WechatGroupMemberSnapshot(member.DisplayName, IsOwner: member.IsOwner)).ToArray(); - } - public static async Task GetGroupMembersPageAsync(string group, int limit = 500, int offset = 0, string? accountId = null, string? keyFile = null, CancellationToken cancellationToken = default) { diff --git a/tests/node-agent/WxAgent.Core.Tests/RemoteControlTests.cs b/tests/node-agent/WxAgent.Core.Tests/RemoteControlTests.cs index 3e441ab..c4f1763 100644 --- a/tests/node-agent/WxAgent.Core.Tests/RemoteControlTests.cs +++ b/tests/node-agent/WxAgent.Core.Tests/RemoteControlTests.cs @@ -98,6 +98,7 @@ public sealed class RemoteReportingTests public async Task ConfigStoreLoadsInvalidConfigAsDenyAndUpdatesOneGlobalVersion() { var path = Path.Combine(Path.GetTempPath(), "wxagent-reporting-" + Guid.NewGuid().ToString("N") + ".json"); + var nodePath = path + ".node.json"; try { await File.WriteAllTextAsync(path, "{\"enabled\":true,\"configVersion\":-1}"); @@ -112,15 +113,15 @@ public sealed class RemoteReportingTests Assert.True(loaded.Enabled); Assert.Equal(4, loaded.ConfigVersion); - var nodePath = Path.Combine(Path.GetDirectoryName(path)!, "remote-node.json"); - var nodeConfig = (new RemoteNodeConfiguration { Reporting = loaded }).WithAudit("reporting.enable"); - await RemoteNodeConfigurationStore.SaveAsync(nodePath, nodeConfig); + await File.WriteAllTextAsync(nodePath, "{\"reporting\":{\"enabled\":true,\"configVersion\":4}}"); var nodeLoaded = await RemoteNodeConfigurationStore.LoadAsync(nodePath); - Assert.Equal("reporting.enable", Assert.Single(nodeLoaded.Audit).Action); + Assert.True(nodeLoaded.Reporting.Enabled); + Assert.Equal(4, nodeLoaded.Reporting.ConfigVersion); } finally { if (File.Exists(path)) File.Delete(path); + if (File.Exists(nodePath)) File.Delete(nodePath); } } } diff --git a/tests/node-agent/WxAgent.Service.Tests/ServiceBoundaryTests.cs b/tests/node-agent/WxAgent.Service.Tests/ServiceBoundaryTests.cs index 4574f96..06d9042 100644 --- a/tests/node-agent/WxAgent.Service.Tests/ServiceBoundaryTests.cs +++ b/tests/node-agent/WxAgent.Service.Tests/ServiceBoundaryTests.cs @@ -28,17 +28,6 @@ public sealed class ServiceBoundaryTests public Task> AccountsAsync(CancellationToken ct) => Task.FromResult>([new("account-1", null, null, null, "fingerprint", false)]); } - [Fact] - public void DatabaseSyncIsAuthorizedByAgentConnectionByDefault() - { - var options = new ServiceOptions - { - CredentialFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), - DataDirectory = Path.GetTempPath() - }; - Assert.True(options.EnableDataSync); - } - [Fact] public async Task StaticUiAndBoundedReadOnlyPagesAreRealProductionRoutes() {