Files

172 lines
7.4 KiB
C#

using System.Collections.Concurrent;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ShiroBot.SDK.Abstractions;
using ShiroBot.SDK.Config;
using ShiroBot.SDK.Core;
using ShiroBot.SDK.Models;
using ShiroBot.SDK.Plugin;
using MyAgent.Core;
using MyAgent.Storage;
using MyAgent.Tools;
[assembly: ShiroBotApiCompatibility("0.9", "0.9")]
namespace Shirobot.Plugin.MyAgent;
[BotPlugin("Shirobot.Plugin.MyAgent", Name = "Shirobot.Plugin.MyAgent", Version = "0.1.0", Description = "QQ group AI agent")]
public sealed class AgentPlugin : PluginBase
{
private IServiceProvider? _services;
private WebApplication? _management;
private bool _managementStarted;
private CancellationTokenSource? _lifetime;
private readonly ConcurrentDictionary<Task, byte> _running = new();
protected override void ConfigureRoutes()
{
GroupCommands.MapAll(OnMessageAsync);
Events.MapWhen<MessageDeletedEvent>(e => e.Platform == "qq" && e.Channel.Type == ChannelType.Group, OnDeletedAsync);
}
protected override async Task LoadAsync()
{
var settings = Context.Config.Load<AgentPluginSettings>();
_lifetime = new CancellationTokenSource();
try
{
_management = AgentManagementServer.Build(Context, settings.ManagementListenUrl);
_services = _management.Services;
await _services.GetRequiredService<global::MyAgent.Agents.MemoryWorker>().StartAsync(_lifetime.Token);
await _services.GetRequiredService<GroupHistorySyncService>().StartAsync(_lifetime.Token);
try
{
await _management.StartAsync(_lifetime.Token);
_managementStarted = true;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
BotLog.Warning($"Agent management API did not start: {ex.Message}");
}
}
catch
{
if (_services is null)
{
_lifetime.Dispose();
_lifetime = null;
if (_management is not null) await _management.DisposeAsync();
_management = null;
}
else
{
await OnUnloadAsync();
}
throw;
}
BotLog.Info("Shirobot.Plugin.MyAgent loaded");
}
private Task OnMessageAsync(MessageEvent message)
{
if (message.Platform != "qq" || message.Channel.Type != ChannelType.Group || message.Sender.Id == message.SelfId)
return Task.CompletedTask;
if (!long.TryParse(message.Channel.Id, out var groupId) || !long.TryParse(message.Sender.Id, out var userId))
return Task.CompletedTask;
var services = _services;
var lifetime = _lifetime;
var context = Context;
if (services is null || lifetime is null || lifetime.IsCancellationRequested) return Task.CompletedTask;
var task = Task.Run(async () =>
{
try
{
using var scope = context.UsePlatform(message.Platform);
var configStore = services.GetRequiredService<LocalConfigStore>();
var cfg = await configStore.ReadAsync(lifetime.Token);
if (cfg.Bot.AllowedGroupIds.Count > 0 && !cfg.Bot.AllowedGroupIds.Contains(groupId)) return;
var changed = false;
if (cfg.Bot.BotUserId <= 0 && long.TryParse(message.SelfId, out var selfId))
{
cfg.Bot.BotUserId = selfId;
changed = true;
}
if (cfg.Bot.DefaultGroupId <= 0)
{
cfg.Bot.DefaultGroupId = groupId;
changed = true;
}
if (changed) await configStore.WriteAsync(cfg, lifetime.Token);
var mentions = message.Segments.OfType<MentionSegment>().Select(m => m.UserId).ToList();
var mentioned = !string.IsNullOrWhiteSpace(message.SelfId) && mentions.Contains(message.SelfId);
mentioned |= message.GetPlainText().Contains(cfg.Bot.BotDisplayName, StringComparison.OrdinalIgnoreCase);
mentioned |= cfg.Bot.BotAliases.Any(alias => message.GetPlainText().Contains(alias, StringComparison.OrdinalIgnoreCase));
var request = new GroupChatUserMessage
{
GroupId = groupId,
UserId = userId,
DisplayName = message.Member?.DisplayName ?? message.Sender.Name ?? message.Sender.Id,
Text = message.GetPlainText(),
IsMentioned = mentioned,
MentionedUserIds = mentions,
Attachments = await ((ShirobotGroupPlatform)services.GetRequiredService<IGroupPlatform>()).ToAttachmentsAsync(
message.Segments, groupId, services.GetRequiredService<DocumentTextExtractor>(), lifetime.Token),
MessageId = message.MessageId,
ReplyToMessageId = message.GetQuote()?.MessageId
};
await foreach (var _ in services.GetRequiredService<AgentWorkflowEngine>().RunGroupAsync(
request,
new AgentWorkflowOptions { PublishToHub = true, SendToPlatform = true, EmitUserEvent = true, EmitStatusEvents = true },
lifetime.Token)) { }
}
catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { }
catch (Exception ex) { BotLog.Error($"Agent message failed: {ex}"); }
}, lifetime.Token);
_running.TryAdd(task, 0);
_ = task.ContinueWith(completed => _running.TryRemove(completed, out _), TaskScheduler.Default);
return Task.CompletedTask;
}
private async Task OnDeletedAsync(MessageDeletedEvent e)
{
if (_services is null || _lifetime is null || _lifetime.IsCancellationRequested) return;
if (!long.TryParse(e.Channel.Id, out var groupId)) return;
await _services.GetRequiredService<GroupChatBridge>().HandleRecallAsync(new GroupChatRecallMessage
{
GroupId = groupId,
UserId = long.TryParse(e.SenderId, out var senderId) ? senderId : 0,
RecallMessageId = e.MessageId
}, _lifetime.Token);
}
protected override async Task OnUnloadAsync()
{
var lifetime = _lifetime;
var services = _services;
if (lifetime is null || services is null) return;
lifetime.Cancel();
try { await Task.WhenAll(_running.Keys).WaitAsync(TimeSpan.FromSeconds(3)); }
catch (OperationCanceledException) { }
catch (TimeoutException) { BotLog.Warning("Agent tasks are still stopping"); }
if (_management is not null)
{
if (_managementStarted) await _management.StopAsync(CancellationToken.None);
await services.GetRequiredService<GroupHistorySyncService>().StopAsync(CancellationToken.None);
await services.GetRequiredService<global::MyAgent.Agents.MemoryWorker>().StopAsync(CancellationToken.None);
await _management.DisposeAsync();
_management = null;
_managementStarted = false;
}
lifetime.Dispose();
_services = null;
_lifetime = null;
}
}
public sealed class AgentPluginSettings
{
[ConfigField("管理 API 监听地址")]
public string ManagementListenUrl { get; set; } = "http://127.0.0.1:5087";
}