172 lines
7.4 KiB
C#
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";
|
|
}
|