diff --git a/HuaJiBot.NET.sln b/HuaJiBot.NET.sln
index 0f25d0c..8ab952f 100644
--- a/HuaJiBot.NET.sln
+++ b/HuaJiBot.NET.sln
@@ -13,6 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.CLI", "src\Hua
{729D022F-6631-401D-9378-BC28A5014772} = {729D022F-6631-401D-9378-BC28A5014772}
{7773E08C-333A-4005-B2D1-ACA9057B31F2} = {7773E08C-333A-4005-B2D1-ACA9057B31F2}
{81546AAF-F44A-49D9-8AD4-C90497E020F1} = {81546AAF-F44A-49D9-8AD4-C90497E020F1}
+ {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3} = {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}
{964D0795-71C8-4E8A-B07E-44967BE2A73A} = {964D0795-71C8-4E8A-B07E-44967BE2A73A}
{AF700E47-CD53-4DC4-9602-AE9606B4F6A5} = {AF700E47-CD53-4DC4-9602-AE9606B4F6A5}
{B1AC8288-59AC-42C9-8669-C61C8781C3C4} = {B1AC8288-59AC-42C9-8669-C61C8781C3C4}
@@ -34,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Plugin.Scripti
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Satori", "src\HuaJiBot.NET.Adapter.Satori\HuaJiBot.NET.Adapter.Satori.csproj", "{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Kook", "src\HuaJiBot.NET.Adapter.Kook\HuaJiBot.NET.Adapter.Kook.csproj", "{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Lagrange", "src\HuaJiBot.NET.Adapter.Lagrange\HuaJiBot.NET.Adapter.Lagrange.csproj", "{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Plugin.AutoReply", "src\HuaJiBot.NET.Plugin.AutoReply\HuaJiBot.NET.Plugin.AutoReply.csproj", "{81546AAF-F44A-49D9-8AD4-C90497E020F1}"
@@ -88,6 +91,10 @@ Global
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Release|Any CPU.Build.0 = Release|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/src/HuaJiBot.NET.Adapter.Kook/HuaJiBot.NET.Adapter.Kook.csproj b/src/HuaJiBot.NET.Adapter.Kook/HuaJiBot.NET.Adapter.Kook.csproj
new file mode 100644
index 0000000..c8a081b
--- /dev/null
+++ b/src/HuaJiBot.NET.Adapter.Kook/HuaJiBot.NET.Adapter.Kook.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/HuaJiBot.NET.Adapter.Kook/KookAdapter.cs b/src/HuaJiBot.NET.Adapter.Kook/KookAdapter.cs
new file mode 100644
index 0000000..a132ecd
--- /dev/null
+++ b/src/HuaJiBot.NET.Adapter.Kook/KookAdapter.cs
@@ -0,0 +1,266 @@
+using HuaJiBot.NET.Bot;
+using HuaJiBot.NET.Commands;
+using HuaJiBot.NET.Events;
+using HuaJiBot.NET.Logger;
+using Kook;
+using Kook.WebSocket;
+
+namespace HuaJiBot.NET.Adapter.Kook;
+
+public class KookAdapter : BotServiceBase
+{
+ private readonly KookSocketClient _client;
+ private readonly string _token;
+
+ public KookAdapter(string token)
+ {
+ _token = token;
+ _client = new KookSocketClient();
+ }
+
+ public override required ILogger Logger { get; init; }
+
+ public override void Reconnect()
+ {
+ _ = Task.Run(async () =>
+ {
+ if (_client.ConnectionState == ConnectionState.Connected)
+ {
+ await _client.StopAsync();
+ }
+ await _client.LoginAsync(TokenType.Bot, _token);
+ await _client.StartAsync();
+ });
+ }
+
+ public override async Task SetupServiceAsync()
+ {
+ await _client.LoginAsync(TokenType.Bot, _token);
+ await _client.StartAsync();
+
+ _client.MessageReceived += OnMessageReceived;
+ _client.Ready += OnReady;
+ }
+
+ private Task OnReady()
+ {
+ Events.CallOnBotLogin(this, new BotLoginEventArgs
+ {
+ Service = this,
+ Accounts = _client.CurrentUser is not null ? [_client.CurrentUser.Id.ToString()] : [],
+ ClientName = "Kook.Net",
+ ClientVersion = "0.0.45-alpha"
+ });
+ Log($"Kook Bot 登录成功!账号:{_client.CurrentUser?.Username}({_client.CurrentUser?.Id})");
+ return Task.CompletedTask;
+ }
+
+ private Task OnMessageReceived(Cacheable cachedMessage, ISocketMessageChannel channel)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var message = cachedMessage.HasValue ? cachedMessage.Value : await cachedMessage.GetOrDownloadAsync();
+
+ // Only process messages from guilds (servers)
+ if (channel is not ITextChannel textChannel || message is not IUserMessage userMessage)
+ return;
+
+ // Ignore bot messages
+ if (message.Author.IsBot)
+ return;
+
+ var guild = textChannel.Guild;
+ var author = message.Author;
+
+ // Create command reader for the message
+ var commandReader = new KookCommandReader(this, message);
+
+ // Create group message event args
+ var eventArgs = new GroupMessageEventArgs(
+ () => commandReader,
+ () => ValueTask.FromResult(textChannel.Name)
+ )
+ {
+ Service = this,
+ RobotId = _client.CurrentUser?.Id.ToString(),
+ MessageId = message.Id.ToString(),
+ GroupId = textChannel.Id.ToString(),
+ SenderId = author.Id.ToString(),
+ SenderMemberCard = author.Username,
+ TextMessageLazy = new Lazy(() => message.Content)
+ };
+
+ // Call the group message received event
+ Events.CallOnGroupMessageReceived(eventArgs);
+ }
+ catch (Exception ex)
+ {
+ LogError("处理消息时出错", ex);
+ }
+ });
+
+ return Task.CompletedTask;
+ }
+
+ public override string[] AllRobots => _client.CurrentUser is not null
+ ? [_client.CurrentUser.Id.ToString()]
+ : [];
+
+ public override async Task SendGroupMessageAsync(
+ string? robotId,
+ string targetGroup,
+ params SendingMessageBase[] messages
+ )
+ {
+ if (!ulong.TryParse(targetGroup, out var channelId))
+ throw new ArgumentException("Invalid channel ID format", nameof(targetGroup));
+
+ var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
+ if (channel == null)
+ throw new ArgumentException("Channel not found", nameof(targetGroup));
+
+ var messageIds = new List();
+
+ foreach (var message in messages)
+ {
+ switch (message)
+ {
+ case TextMessage textMessage:
+ var sentMessage = await channel.SendTextAsync(textMessage.Text);
+ messageIds.Add(sentMessage.Id.ToString());
+ break;
+ case AtMessage atMessage:
+ if (ulong.TryParse(atMessage.Target, out var userId))
+ {
+ var sentAtMessage = await channel.SendTextAsync($"(met){userId}(met)");
+ messageIds.Add(sentAtMessage.Id.ToString());
+ }
+ break;
+ case ImageMessage imageMessage:
+ // TODO: Implement image sending
+ LogDebug($"图片消息暂未实现: {imageMessage.ImagePath}");
+ break;
+ case ReplyMessage replyMessage:
+ // TODO: Implement reply message
+ LogDebug($"回复消息暂未实现: {replyMessage.MessageId}");
+ break;
+ default:
+ LogDebug($"未支持的消息类型: {message.GetType().Name}");
+ break;
+ }
+ }
+
+ return messageIds.ToArray();
+ }
+
+ public override void RecallMessage(string? robotId, string targetGroup, string msgId)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ if (!ulong.TryParse(targetGroup, out var channelId) || !Guid.TryParse(msgId, out var messageId))
+ return;
+
+ var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
+ if (channel == null)
+ return;
+
+ var message = await channel.GetMessageAsync(messageId);
+ if (message != null)
+ {
+ await message.DeleteAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ LogError("撤回消息失败", ex);
+ }
+ });
+ }
+
+ public override void SetGroupName(string? robotId, string targetGroup, string groupName)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ if (!ulong.TryParse(targetGroup, out var channelId))
+ return;
+
+ var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
+ if (channel == null)
+ return;
+
+ await channel.ModifyAsync(x => x.Name = groupName);
+ }
+ catch (Exception ex)
+ {
+ LogError("修改频道名称失败", ex);
+ }
+ });
+ }
+
+ public override MemberType GetMemberType(string robotId, string targetGroup, string userId)
+ {
+ // TODO: Implement member type retrieval based on Kook roles
+ return MemberType.Member;
+ }
+
+ public override async Task FeedbackAt(
+ string? robotId,
+ string targetGroup,
+ string msgId,
+ string text
+ )
+ {
+ if (!ulong.TryParse(targetGroup, out var channelId))
+ return [];
+
+ var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
+ if (channel == null)
+ return [];
+
+ try
+ {
+ // Try to get the original message to find the sender
+ if (Guid.TryParse(msgId, out var messageId))
+ {
+ var originalMessage = await channel.GetMessageAsync(messageId);
+ if (originalMessage != null)
+ {
+ // Create a mention for the original sender
+ var mention = $"(met){originalMessage.Author.Id}(met)";
+ var replyText = $"{mention} {text}";
+ var sentMessage = await channel.SendTextAsync(replyText);
+ return [sentMessage.Id.ToString()];
+ }
+ }
+
+ // Fallback: just send the text without mention
+ var fallbackMessage = await channel.SendTextAsync(text);
+ return [fallbackMessage.Id.ToString()];
+ }
+ catch (Exception ex)
+ {
+ LogError("发送回复消息失败", ex);
+ return [];
+ }
+ }
+
+ public override string GetNick(string robotId, string userId)
+ {
+ // TODO: Implement nickname retrieval
+ return "";
+ }
+
+ public override string GetPluginDataPath()
+ {
+ var path = Path.GetFullPath(Path.Combine("plugins", "data"));
+ if (!Directory.Exists(path))
+ Directory.CreateDirectory(path);
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/src/HuaJiBot.NET.Adapter.Kook/KookCommandReader.cs b/src/HuaJiBot.NET.Adapter.Kook/KookCommandReader.cs
new file mode 100644
index 0000000..671cee8
--- /dev/null
+++ b/src/HuaJiBot.NET.Adapter.Kook/KookCommandReader.cs
@@ -0,0 +1,72 @@
+using HuaJiBot.NET.Bot;
+using HuaJiBot.NET.Commands;
+using Kook;
+using static HuaJiBot.NET.Commands.CommonCommandReader;
+
+namespace HuaJiBot.NET.Adapter.Kook;
+
+///
+/// Kook 指令读取器
+///
+internal class KookCommandReader(BotService service, IMessage message) : CommonCommandReader
+{
+ public override IEnumerable Msg
+ {
+ get
+ {
+ return Parse();
+
+ IEnumerable Parse()
+ {
+ // Parse the message content - Kook uses KMarkdown format
+ var content = message.Content;
+
+ if (string.IsNullOrEmpty(content))
+ yield break;
+
+ // Parse mentions in the format (met)userId(met)
+ var mentionPattern = @"\(met\)(\d+)\(met\)";
+ var matches = System.Text.RegularExpressions.Regex.Matches(content, mentionPattern);
+
+ var processedContent = content;
+ foreach (System.Text.RegularExpressions.Match match in matches)
+ {
+ var userId = match.Groups[1].Value;
+ processedContent = processedContent.Replace(match.Value, "");
+
+ // Get the username if possible
+ var username = "";
+ if (message is IUserMessage userMessage && userMessage.MentionedUsers.Any())
+ {
+ var mentionedUser = userMessage.MentionedUsers.FirstOrDefault(u => u.Id.ToString() == userId);
+ username = mentionedUser?.Username ?? "";
+ }
+
+ yield return new ReaderAt(userId, username);
+ }
+
+ // Return the text content without mentions
+ var trimmedContent = processedContent.Trim();
+ if (!string.IsNullOrEmpty(trimmedContent))
+ {
+ yield return trimmedContent;
+ }
+
+ // Handle quote/reply if reference exists
+ if (message.Reference.HasValue && message.Reference.Value.MessageId.HasValue)
+ {
+ var refMessageId = message.Reference.Value.MessageId.Value.ToString();
+ yield return new ReaderReply(new CommandReader.ReplyInfo(
+ messageId: refMessageId,
+ seqId: null,
+ senderId: null,
+ content: null
+ ));
+ }
+
+ // TODO: Handle other Kook-specific message types like cards, embeds, attachments, etc.
+ // Kook supports rich KMarkdown content that could be parsed here
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HuaJiBot.NET.CLI/App.cs b/src/HuaJiBot.NET.CLI/App.cs
index dee2cdb..cdf63b2 100644
--- a/src/HuaJiBot.NET.CLI/App.cs
+++ b/src/HuaJiBot.NET.CLI/App.cs
@@ -1,6 +1,7 @@
using HuaJiBot.NET;
using HuaJiBot.NET.Adapter.OneBot;
using HuaJiBot.NET.Adapter.Satori;
+using HuaJiBot.NET.Adapter.Kook;
using HuaJiBot.NET.Bot;
using HuaJiBot.NET.Config;
using HuaJiBot.NET.Logger;
@@ -32,12 +33,19 @@ BotServiceBase CreateSatoriService(Config config)
return api;
}
+BotServiceBase CreateKookService(Config config)
+{
+ var api = new KookAdapter(config.Kook.Token) { Logger = logger }; //链接协议适配器
+ return api;
+}
+
BotServiceBase CreateService(Config config)
{
return config.Service switch
{
Config.ServiceType.OneBot => CreateOneBotService(config),
Config.ServiceType.Satori => CreateSatoriService(config),
+ Config.ServiceType.Kook => CreateKookService(config),
_ => throw new NotSupportedException("不支持的协议类型"),
};
}
diff --git a/src/HuaJiBot.NET.CLI/HuaJiBot.NET.CLI.csproj b/src/HuaJiBot.NET.CLI/HuaJiBot.NET.CLI.csproj
index 99c034a..61962ca 100644
--- a/src/HuaJiBot.NET.CLI/HuaJiBot.NET.CLI.csproj
+++ b/src/HuaJiBot.NET.CLI/HuaJiBot.NET.CLI.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/HuaJiBot.NET/Config/Config.cs b/src/HuaJiBot.NET/Config/Config.cs
index ea1e6be..f02fd42 100644
--- a/src/HuaJiBot.NET/Config/Config.cs
+++ b/src/HuaJiBot.NET/Config/Config.cs
@@ -10,7 +10,8 @@ public partial class Config
public enum ServiceType
{
OneBot,
- Satori
+ Satori,
+ Kook
}
public ServiceType Service = ServiceType.OneBot;
@@ -30,6 +31,13 @@ public class SatoriConnectionInfo
}
public SatoriConnectionInfo Satori = new();
+
+ public class KookConnectionInfo
+ {
+ public string Token = "";
+ }
+
+ public KookConnectionInfo Kook = new();
public string[] ExtraPlugins { get; set; } = [];
public Dictionary Plugins = new();