feat: add control-plane identity and management console
Build web service image / build (push) Successful in 2m7s
Build web service image / build (push) Successful in 2m7s
This commit is contained in:
@@ -4,6 +4,34 @@ namespace WxAgent.Core;
|
||||
|
||||
public static class ReportingAuthorization
|
||||
{
|
||||
public static ReportingConfig ForVerifiedAccounts(ReportingConfig configured, IEnumerable<string> verifiedAccountIds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configured);
|
||||
ArgumentNullException.ThrowIfNull(verifiedAccountIds);
|
||||
|
||||
var accounts = verifiedAccountIds
|
||||
.Where(accountId => !string.IsNullOrWhiteSpace(accountId))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Select(accountId => new AccountReportingConfig
|
||||
{
|
||||
AccountId = accountId,
|
||||
Enabled = true,
|
||||
AllowedChats =
|
||||
[
|
||||
new AllowedChat { Type = ReportingChatType.Group, ChatId = "*", Enabled = true, IdentityVerified = true },
|
||||
new AllowedChat { Type = ReportingChatType.Private, ChatId = "*", Enabled = true, IdentityVerified = true }
|
||||
]
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return configured with
|
||||
{
|
||||
Enabled = true,
|
||||
ConfigVersion = Math.Max(1, configured.ConfigVersion),
|
||||
Accounts = accounts
|
||||
};
|
||||
}
|
||||
|
||||
public static ReportingDecision Check(
|
||||
ReportingConfig? config,
|
||||
string accountId,
|
||||
|
||||
@@ -17,12 +17,21 @@ public sealed class EventPump(IAgentBackend backend, EventHub hub, ServiceOption
|
||||
{
|
||||
try
|
||||
{
|
||||
var remote = options.Remote;
|
||||
var reporting = options.Reporting;
|
||||
if (remoteQueue is not null && remote is { IsConfigured: true })
|
||||
{
|
||||
var accounts = await backend.AccountsAsync(stoppingToken, refreshUiIdentity: false);
|
||||
reporting = ReportingAuthorization.ForVerifiedAccounts(options.Reporting,
|
||||
accounts.Where(RemoteAgentHostedService.HasLiveWeChatBinding).Select(account => account.AccountId));
|
||||
}
|
||||
|
||||
await foreach (var item in source.ListenAsync(stoppingToken))
|
||||
{
|
||||
if (remoteQueue is not null && options.Remote is { IsConfigured: true } remote && item.Kind == "message"
|
||||
if (remoteQueue is not null && remote is { IsConfigured: true } && item.Kind == "message"
|
||||
&& item.ChatId is { Length: > 0 } chatId && item.ChatType is { } chatType)
|
||||
{
|
||||
_ = remoteQueue.Enqueue(options.Reporting, remote.NodeId!, item.AccountId, chatId, chatType,
|
||||
_ = remoteQueue.Enqueue(reporting, remote.NodeId!, item.AccountId, chatId, chatType,
|
||||
item.Kind, item.At, item.Content);
|
||||
}
|
||||
var localItem = item with { Content = null };
|
||||
|
||||
@@ -876,12 +876,11 @@ public sealed class RemoteAgentHostedService(
|
||||
// the explicit accounts API remains the only path that may inspect the profile.
|
||||
var accounts = await backend.AccountsAsync(cancellationToken, refreshUiIdentity: false);
|
||||
var identities = accounts.Select(account => new RemoteAccountIdentity(
|
||||
account.AccountId,
|
||||
account.IsUiBindingKnown && account.Binding is not null && string.Equals(account.BindingStatus, "Bound", StringComparison.Ordinal))).ToArray();
|
||||
account.AccountId, HasLiveWeChatBinding(account))).ToArray();
|
||||
var activeAccountId = remote.ActiveAccountId;
|
||||
var boundAccounts = accounts
|
||||
.Where(account => account.IsUiBindingKnown && account.Binding is not null && string.Equals(account.BindingStatus, "Bound", StringComparison.Ordinal))
|
||||
.Select(account => account.AccountId)
|
||||
var boundAccounts = identities
|
||||
.Where(identity => identity.Verified)
|
||||
.Select(identity => identity.AccountId)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if ((string.IsNullOrWhiteSpace(activeAccountId) || !identities.Any(identity => identity.Verified && string.Equals(identity.AccountId, activeAccountId, StringComparison.Ordinal)))
|
||||
@@ -922,30 +921,16 @@ public sealed class RemoteAgentHostedService(
|
||||
|
||||
private static ReportingConfig ConnectionAuthorizedReporting(ReportingConfig configured, BackendSnapshot? snapshot)
|
||||
{
|
||||
var accountIds = snapshot is not null
|
||||
? snapshot.Accounts.Where(identity => identity.Verified).Select(identity => identity.AccountId)
|
||||
: configured.Accounts.Where(account => account.Enabled).Select(account => account.AccountId);
|
||||
var accounts = accountIds
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Select(accountId => new AccountReportingConfig
|
||||
{
|
||||
AccountId = accountId,
|
||||
Enabled = true,
|
||||
AllowedChats =
|
||||
[
|
||||
new AllowedChat { Type = ReportingChatType.Group, ChatId = "*", Enabled = true, IdentityVerified = true },
|
||||
new AllowedChat { Type = ReportingChatType.Private, ChatId = "*", Enabled = true, IdentityVerified = true }
|
||||
]
|
||||
})
|
||||
.ToArray();
|
||||
return configured with
|
||||
{
|
||||
Enabled = true,
|
||||
ConfigVersion = Math.Max(1, configured.ConfigVersion),
|
||||
Accounts = accounts
|
||||
};
|
||||
var accountIds = snapshot?.Accounts
|
||||
.Where(identity => identity.Verified)
|
||||
.Select(identity => identity.AccountId) ?? Enumerable.Empty<string>();
|
||||
return ReportingAuthorization.ForVerifiedAccounts(configured, accountIds);
|
||||
}
|
||||
|
||||
internal static bool HasLiveWeChatBinding(AccountInfo account) =>
|
||||
account.IsUiBindingKnown && account.Binding is not null &&
|
||||
string.Equals(account.BindingStatus, "Bound", StringComparison.Ordinal);
|
||||
|
||||
private static RemoteNodeRegistration CreateRegistration(RemoteAgentOptions remote, ReportingConfig reporting, BackendSnapshot snapshot) =>
|
||||
new(remote.NodeId!, typeof(RemoteAgentHostedService).Assembly.GetName().Version?.ToString() ?? "dev",
|
||||
RemoteProtocol.Version, ["heartbeat", "poll-tasks", "send-text", "read-sessions", "read-contacts", "read-messages", "db-messages", "db-merged", "report-message"], reporting.ConfigVersion,
|
||||
|
||||
@@ -366,23 +366,19 @@ internal static class ServiceSettingsEditor
|
||||
var token = current.AccessToken ?? ServiceOptions.GenerateToken();
|
||||
var remote = current.Remote;
|
||||
var reporting = (current.Reporting ?? new ReportingConfig()).NormalizeAndValidate();
|
||||
var reportingAccount = reporting.Accounts.FirstOrDefault();
|
||||
var boundAccounts = new AccountBindingStore(current).ReadAll();
|
||||
var reportingAccountId = reportingAccount?.AccountId
|
||||
?? (boundAccounts.Count == 1 ? boundAccounts[0].AccountId : "");
|
||||
ServiceOptions? result = null;
|
||||
using var form = new Form
|
||||
{
|
||||
Text = "WxAgent 服务设置",
|
||||
Width = 720,
|
||||
Height = 805,
|
||||
Height = 660,
|
||||
StartPosition = FormStartPosition.CenterScreen,
|
||||
MinimizeBox = false,
|
||||
MaximizeBox = false,
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog
|
||||
};
|
||||
|
||||
var tabs = new TabControl { Left = 12, Top = 12, Width = 680, Height = 720 };
|
||||
var tabs = new TabControl { Left = 12, Top = 12, Width = 680, Height = 560 };
|
||||
var localTab = new TabPage("本地服务");
|
||||
var remoteTab = new TabPage("远程连接");
|
||||
tabs.TabPages.Add(localTab);
|
||||
@@ -467,82 +463,34 @@ internal static class ServiceSettingsEditor
|
||||
var reportingTitle = new Label
|
||||
{
|
||||
Left = 18, Top = 416, Width = 620, Height = 24,
|
||||
Text = "远程读取授权(Agent 连接后自动授予已验证账号和会话)"
|
||||
};
|
||||
var reportingEnabled = new CheckBox
|
||||
{
|
||||
Left = 18, Top = 442, Width = 620,
|
||||
Text = "使用 Agent 连接授权读取(无需单独确认)",
|
||||
Checked = true,
|
||||
Enabled = false
|
||||
};
|
||||
var reportingAccountEnabled = new CheckBox
|
||||
{
|
||||
Left = 150, Top = 470, Width = 485,
|
||||
Text = "启用当前账号",
|
||||
Checked = reportingAccount?.Enabled ?? true
|
||||
};
|
||||
var reportingAccountLabel = new Label { Left = 18, Top = 505, Width = 125, Text = "账号 ID" };
|
||||
var reportingAccountBox = new TextBox { Left = 150, Top = 501, Width = 485, Text = reportingAccountId };
|
||||
var groupChatsLabel = new Label { Left = 18, Top = 541, Width = 125, Text = "群聊白名单" };
|
||||
var groupChatsBox = new TextBox
|
||||
{
|
||||
Left = 150, Top = 537, Width = 485, Height = 48,
|
||||
Multiline = true, AcceptsReturn = true, ScrollBars = ScrollBars.Vertical,
|
||||
Text = string.Join(Environment.NewLine, reportingAccount?.AllowedChats
|
||||
.Where(chat => chat.Type == ReportingChatType.Group).Select(chat => chat.ChatId) ?? [])
|
||||
};
|
||||
var privateChatsLabel = new Label { Left = 18, Top = 593, Width = 125, Text = "私聊白名单" };
|
||||
var privateChatsBox = new TextBox
|
||||
{
|
||||
Left = 150, Top = 589, Width = 485, Height = 48,
|
||||
Multiline = true, AcceptsReturn = true, ScrollBars = ScrollBars.Vertical,
|
||||
Text = string.Join(Environment.NewLine, reportingAccount?.AllowedChats
|
||||
.Where(chat => chat.Type == ReportingChatType.Private).Select(chat => chat.ChatId) ?? [])
|
||||
};
|
||||
var reportingIdentityConfirmed = new CheckBox
|
||||
{
|
||||
Left = 150, Top = 645, Width = 485,
|
||||
Text = "确认上述 chatId 已与微信身份核对",
|
||||
Checked = reportingAccount is not null && reportingAccount.AllowedChats.Count > 0 && reportingAccount.AllowedChats.All(chat => chat.IdentityVerified)
|
||||
Text = "授权方式"
|
||||
};
|
||||
var reportingNote = new Label
|
||||
{
|
||||
Left = 18, Top = 674, Width = 620, Height = 36,
|
||||
Text = "连接成功即授权当前已验证账号的通讯录、会话和消息读取;下方旧范围仅为兼容显示。"
|
||||
Left = 18, Top = 446, Width = 620, Height = 82,
|
||||
Text = "连接控制面后,所有已完成身份绑定且当前绑定有效的账号均获得通讯录、会话和消息的只读同步授权,无需维护账号或聊天白名单。启用上方后台消息监听后,Agent 仅推送监听器实际采集到的实时事件。"
|
||||
};
|
||||
remoteTab.Controls.AddRange([
|
||||
remoteEnabled, remoteAddressLabel, remoteAddressBox, remoteNodeLabel, remoteNodeBox,
|
||||
remoteAccountLabel, remoteAccountBox, remoteTokenLabel, remoteTokenBox,
|
||||
remoteTokenFileLabel, remoteTokenFileBox, allowInsecureHttp, remoteServerCaLabel,
|
||||
remoteServerCaBox, remoteClientCertLabel, remoteClientCertBox, remoteClientKeyLabel,
|
||||
remoteClientKeyBox, remoteNote, reportingTitle, reportingEnabled, reportingAccountEnabled,
|
||||
reportingAccountLabel, reportingAccountBox, groupChatsLabel, groupChatsBox,
|
||||
privateChatsLabel, privateChatsBox, reportingIdentityConfirmed, reportingNote
|
||||
remoteClientKeyBox, remoteNote, reportingTitle, reportingNote
|
||||
]);
|
||||
var remoteInputs = new Control[]
|
||||
{
|
||||
remoteAddressBox, remoteNodeBox, remoteAccountBox, remoteTokenBox, remoteTokenFileBox,
|
||||
allowInsecureHttp, remoteServerCaBox, remoteClientCertBox, remoteClientKeyBox
|
||||
};
|
||||
var reportingInputs = new Control[]
|
||||
{
|
||||
reportingAccountEnabled, reportingAccountBox, groupChatsBox, privateChatsBox,
|
||||
reportingIdentityConfirmed
|
||||
};
|
||||
void SetRemoteEnabled()
|
||||
{
|
||||
foreach (var control in remoteInputs) control.Enabled = remoteEnabled.Checked;
|
||||
reportingEnabled.Enabled = remoteEnabled.Checked;
|
||||
var reportingInputsEnabled = false;
|
||||
foreach (var control in reportingInputs) control.Enabled = reportingInputsEnabled;
|
||||
}
|
||||
remoteEnabled.CheckedChanged += (_, _) => SetRemoteEnabled();
|
||||
reportingEnabled.CheckedChanged += (_, _) => SetRemoteEnabled();
|
||||
SetRemoteEnabled();
|
||||
|
||||
var save = new Button { Left = 470, Top = 740, Width = 105, Text = "保存" };
|
||||
var cancel = new Button { Left = 585, Top = 740, Width = 105, Text = "取消" };
|
||||
var save = new Button { Left = 470, Top = 585, Width = 105, Text = "保存" };
|
||||
var cancel = new Button { Left = 585, Top = 585, Width = 105, Text = "取消" };
|
||||
copy.Click += (_, _) => { Clipboard.SetText(tokenBox.Text); copy.Text = "已复制"; };
|
||||
regenerate.Click += (_, _) =>
|
||||
{
|
||||
@@ -573,9 +521,9 @@ internal static class ServiceSettingsEditor
|
||||
AllowInsecureHttp = allowInsecureHttp.Checked
|
||||
}
|
||||
: null;
|
||||
var reportingOptions = remoteEnabled.Checked
|
||||
? reporting with { Enabled = true, ConfigVersion = checked(Math.Max(1, reporting.ConfigVersion) + 1) }
|
||||
: reporting;
|
||||
var reportingOptions = reporting with { Accounts = [] };
|
||||
if (remoteEnabled.Checked)
|
||||
reportingOptions = reportingOptions with { Enabled = true, ConfigVersion = checked(Math.Max(1, reporting.ConfigVersion) + 1) };
|
||||
var edited = new ServiceOptions
|
||||
{
|
||||
ListenUrl = $"http://{formattedHost}:{portBox.Value}",
|
||||
|
||||
Reference in New Issue
Block a user