diff --git a/Docs/Bot_Commands_User_Guide.md b/Docs/Bot_Commands_User_Guide.md
index 34f60ef7..05b16083 100644
--- a/Docs/Bot_Commands_User_Guide.md
+++ b/Docs/Bot_Commands_User_Guide.md
@@ -63,6 +63,16 @@
* **权限**: 普通管理员或全局管理员。
* **相关指令**: `选择音乐模型` 显示可用音乐模型并通过编号选择,`音乐模型` / `查看音乐模型` 查看当前音乐模型,`清除音乐模型` 恢复使用全局默认。
+* **Agent 聊天模式**
+ * **指令格式**: `开启Agent聊天` / `开启Agent引导聊天`
+ * **功能**: 在当前群开启引导模式。普通文本、Caption、OCR/QR/ASR 提取结果会在短时间窗口内合并成一次 Agent 输入并自动回复。
+ * **指令格式**: `开启Agent队列聊天`
+ * **功能**: 在当前群开启队列模式。每条触发消息都会按顺序进入 Agent 队列并分别回复。
+ * **指令格式**: `关闭Agent聊天` / `Agent聊天状态` / `查看Agent聊天`
+ * **功能**: 关闭或查看当前群 Agent 聊天模式。
+ * **权限**: 普通管理员或全局管理员。
+ * **要求**: 需要已设置群 LLM 模型,并启用 `EnableLLMAgentProcess=true`。
+
### 2. 全局管理员指令
以下指令仅限机器人全局管理员(在 `Env.cs` 中配置的 `AdminId`)使用。
@@ -355,6 +365,7 @@
* **触发方式1 (提及机器人)**: 在消息中 `@机器人用户名 <你的消息>`。
* **示例**: `@MyAwesomeBot 帮我总结一下这篇文章的主要内容。`
* **触发方式2 (回复机器人消息)**: 直接回复机器人的某条消息。
+ * **触发方式3 (Agent 聊天模式)**: 管理员开启 `开启Agent聊天` / `开启Agent引导聊天` 或 `开启Agent队列聊天` 后,群内普通文本、Caption 和媒体提取结果会自动触发 LLM。
* **功能**: 将您的消息发送给配置的LLM进行处理,并回复LLM的响应。
* **工具调用迭代限制**: 为防止LLM无限调用工具,机器人默认限制为25次工具调用(可通过 `MaxToolCycles` 配置)。当达到限制时:
* 机器人会保存当前对话快照到Redis
diff --git a/README.md b/README.md
index 7b10f829..01b7d9a5 100644
--- a/README.md
+++ b/README.md
@@ -180,6 +180,9 @@
### AI交互
- @机器人 + 问题: 使用配置的LLM回复
+- 管理员发送 `开启Agent聊天` / `开启Agent引导聊天`: 当前群进入引导模式,普通文本、Caption 以及 OCR/QR/ASR 提取内容会短窗口合并后自动触发 Agent 回复
+- 管理员发送 `开启Agent队列聊天`: 当前群进入逐条队列模式,每条触发消息按顺序进入 Agent 队列
+- 管理员发送 `关闭Agent聊天` / `Agent聊天状态` / `查看Agent聊天`: 关闭或查看当前群 Agent 聊天模式
完整命令列表: [Docs/Bot_Commands_User_Guide.md](Docs/Bot_Commands_User_Guide.md)
diff --git a/TelegramSearchBot.Common/Model/AI/LlmAgentContracts.cs b/TelegramSearchBot.Common/Model/AI/LlmAgentContracts.cs
index da67a6e4..71645794 100644
--- a/TelegramSearchBot.Common/Model/AI/LlmAgentContracts.cs
+++ b/TelegramSearchBot.Common/Model/AI/LlmAgentContracts.cs
@@ -224,6 +224,7 @@ public static class LlmAgentRedisKeys {
public const string ActiveTaskSet = "AGENT_ACTIVE_TASKS";
public const string SubAgentTaskQueue = "SUBAGENT_TASKS";
public const string AgentToolDefs = "AGENT_TOOL_DEFS";
+ public const string AgentChatBatchDueSet = "AGENT_CHAT_BATCH_DUE";
public static string AgentTaskState(string taskId) => $"AGENT_TASK:{taskId}";
public static string AgentSnapshot(string taskId) => $"AGENT_SNAPSHOT:{taskId}";
@@ -239,5 +240,13 @@ public static class LlmAgentRedisKeys {
public static string SandboxToolQueue(long chatId) => $"SANDBOX_TOOL_TASKS:{chatId}";
public static string SandboxToolHeartbeat(long chatId) => $"SANDBOX_TOOL_HEARTBEAT:{chatId}";
public static string SandboxToolResult(string requestId) => $"SANDBOX_TOOL_RESULT:{requestId}";
+ public static string ModelSelectState(long chatId) => $"modelselect:{chatId}:state";
+ public static string ModelSelectModels(long chatId) => $"modelselect:{chatId}:models";
+ public static string ImageGenerationModelSelection(long chatId, long userId) => $"image_generation:model_select:{chatId}:{userId}";
+ public static string MusicGenerationModelSelection(long chatId, long userId) => $"music_generation:model_select:{chatId}:{userId}";
+ public static string AgentChatBatchList(long chatId) => $"AGENT_CHAT_BATCH:{chatId}:MESSAGES";
+ public static string AgentChatBatchMeta(long chatId) => $"AGENT_CHAT_BATCH:{chatId}:META";
+ public static string AgentChatBatchLock(long chatId) => $"AGENT_CHAT_BATCH:{chatId}:LOCK";
+ public static string AgentChatConfigWarning(long chatId, string warningType) => $"AGENT_CHAT_WARNING:{chatId}:{warningType}";
}
}
diff --git a/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.Designer.cs b/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.Designer.cs
new file mode 100644
index 00000000..956a0837
--- /dev/null
+++ b/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.Designer.cs
@@ -0,0 +1,893 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using TelegramSearchBot.Model;
+
+#nullable disable
+
+namespace TelegramSearchBot.Migrations
+{
+ [DbContext(typeof(DataDbContext))]
+ [Migration("20260531153140_AddGroupAgentChatMode")]
+ partial class AddGroupAgentChatMode
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.AccountBook", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId", "Name")
+ .IsUnique();
+
+ b.ToTable("AccountBooks");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.AccountRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccountBookId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedByUsername")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("Tag")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag");
+
+ b.HasIndex("AccountBookId", "CreatedAt");
+
+ b.ToTable("AccountRecords");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.AppConfigurationItem", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("AppConfigurationItems");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ChannelWithModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("IsDeleted")
+ .HasColumnType("INTEGER");
+
+ b.Property("LLMChannelId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ModelName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LLMChannelId");
+
+ b.ToTable("ChannelsWithModel");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ConversationSegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ContentSummary")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EndTime")
+ .HasColumnType("TEXT");
+
+ b.Property("FirstMessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FullContent")
+ .HasColumnType("TEXT");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsVectorized")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastMessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MessageCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ParticipantCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("StartTime")
+ .HasColumnType("TEXT");
+
+ b.Property("TopicKeywords")
+ .HasColumnType("TEXT");
+
+ b.Property("VectorId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId", "StartTime", "EndTime");
+
+ b.ToTable("ConversationSegments");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ConversationSegmentMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConversationSegmentId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MessageDataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SequenceOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ConversationSegmentId");
+
+ b.HasIndex("MessageDataId");
+
+ b.ToTable("ConversationSegmentMessages");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.FaissIndexFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Dimension")
+ .HasColumnType("INTEGER");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("FileSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsValid")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("VectorCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId", "IndexType")
+ .IsUnique();
+
+ b.ToTable("FaissIndexFiles");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.GroupAccountSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ActiveAccountBookId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAccountingEnabled")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId")
+ .IsUnique();
+
+ b.ToTable("GroupAccountSettings");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.GroupData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("IsBlacklist")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsForum")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("GroupData");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.GroupSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgentChatBatchWindowSeconds")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgentChatMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImageGenerationModelName")
+ .HasColumnType("TEXT");
+
+ b.Property("IsAgentChatEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsManagerGroup")
+ .HasColumnType("INTEGER");
+
+ b.Property("LLMModelName")
+ .HasColumnType("TEXT");
+
+ b.Property("MusicGenerationModelName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId")
+ .IsUnique();
+
+ b.ToTable("GroupSettings");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.LLMChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("Gateway")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Parallel")
+ .HasColumnType("INTEGER");
+
+ b.Property("Priority")
+ .HasColumnType("INTEGER");
+
+ b.Property("Provider")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("LLMChannels");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.MemoryGraph", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChatId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedTime")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FromEntity")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Observations")
+ .HasColumnType("TEXT");
+
+ b.Property("RelationType")
+ .HasColumnType("TEXT");
+
+ b.Property("ToEntity")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("MemoryGraphs");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.Message", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Content")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTime")
+ .HasColumnType("TEXT");
+
+ b.Property("FromUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReplyToMessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReplyToUserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Messages");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.MessageExtension", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("MessageDataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MessageDataId");
+
+ b.ToTable("MessageExtensions");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ModelCapability", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CapabilityName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CapabilityValue")
+ .HasColumnType("TEXT");
+
+ b.Property("ChannelWithModelId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("LastUpdated")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelWithModelId");
+
+ b.ToTable("ModelCapabilities");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ScheduledTaskExecution", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CompletedTime")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("LastHeartbeat")
+ .HasColumnType("TEXT");
+
+ b.Property("ResultSummary")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("StartTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT");
+
+ b.Property("TaskName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TaskName")
+ .IsUnique();
+
+ b.ToTable("ScheduledTaskExecutions");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.SearchPageCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedTime")
+ .HasColumnType("TEXT");
+
+ b.Property("SearchOptionJson")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UUID")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SearchPageCaches");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ShortUrlMapping", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreationDate")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpandedUrl")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalUrl")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OriginalUrl");
+
+ b.ToTable("ShortUrlMappings");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.TelegramFileCacheEntry", b =>
+ {
+ b.Property("CacheKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("TEXT");
+
+ b.Property("FileId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("CacheKey");
+
+ b.HasIndex("CacheKey")
+ .IsUnique();
+
+ b.ToTable("TelegramFileCacheEntries");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.TodoItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChatId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CompletedAtUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("CompletedBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("DueAtUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Priority")
+ .HasMaxLength(20)
+ .HasColumnType("TEXT");
+
+ b.Property("RemindAtUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("ReminderMessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReminderSentAtUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("SourceMessageId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("TodoListId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TodoListId", "Status");
+
+ b.HasIndex("ChatId", "Status", "RemindAtUtc", "ReminderSentAtUtc");
+
+ b.ToTable("TodoItems");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.TodoList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChatId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatId", "Name")
+ .IsUnique();
+
+ b.ToTable("TodoLists");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.UserData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("FirstName")
+ .HasColumnType("TEXT");
+
+ b.Property("IsBot")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsPremium")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.UserWithGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "GroupId")
+ .IsUnique();
+
+ b.ToTable("UsersWithGroup");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.VectorIndex", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ContentSummary")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FaissIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("VectorType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId", "FaissIndex");
+
+ b.HasIndex("GroupId", "VectorType", "EntityId")
+ .IsUnique();
+
+ b.ToTable("VectorIndexes");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.AccountRecord", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.AccountBook", "AccountBook")
+ .WithMany("Records")
+ .HasForeignKey("AccountBookId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AccountBook");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ChannelWithModel", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.LLMChannel", "LLMChannel")
+ .WithMany("Models")
+ .HasForeignKey("LLMChannelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LLMChannel");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ConversationSegmentMessage", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.ConversationSegment", "ConversationSegment")
+ .WithMany("Messages")
+ .HasForeignKey("ConversationSegmentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("TelegramSearchBot.Model.Data.Message", "Message")
+ .WithMany()
+ .HasForeignKey("MessageDataId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ConversationSegment");
+
+ b.Navigation("Message");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.MessageExtension", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.Message", "Message")
+ .WithMany("MessageExtensions")
+ .HasForeignKey("MessageDataId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Message");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ModelCapability", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.ChannelWithModel", "ChannelWithModel")
+ .WithMany("Capabilities")
+ .HasForeignKey("ChannelWithModelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ChannelWithModel");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.TodoItem", b =>
+ {
+ b.HasOne("TelegramSearchBot.Model.Data.TodoList", "TodoList")
+ .WithMany("Items")
+ .HasForeignKey("TodoListId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("TodoList");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.AccountBook", b =>
+ {
+ b.Navigation("Records");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ChannelWithModel", b =>
+ {
+ b.Navigation("Capabilities");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.ConversationSegment", b =>
+ {
+ b.Navigation("Messages");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.LLMChannel", b =>
+ {
+ b.Navigation("Models");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.Message", b =>
+ {
+ b.Navigation("MessageExtensions");
+ });
+
+ modelBuilder.Entity("TelegramSearchBot.Model.Data.TodoList", b =>
+ {
+ b.Navigation("Items");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.cs b/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.cs
new file mode 100644
index 00000000..4a9534a7
--- /dev/null
+++ b/TelegramSearchBot.Database/Migrations/20260531153140_AddGroupAgentChatMode.cs
@@ -0,0 +1,50 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace TelegramSearchBot.Migrations
+{
+ ///
+ public partial class AddGroupAgentChatMode : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AgentChatBatchWindowSeconds",
+ table: "GroupSettings",
+ type: "INTEGER",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AgentChatMode",
+ table: "GroupSettings",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "IsAgentChatEnabled",
+ table: "GroupSettings",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "AgentChatBatchWindowSeconds",
+ table: "GroupSettings");
+
+ migrationBuilder.DropColumn(
+ name: "AgentChatMode",
+ table: "GroupSettings");
+
+ migrationBuilder.DropColumn(
+ name: "IsAgentChatEnabled",
+ table: "GroupSettings");
+ }
+ }
+}
diff --git a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs
index e1a3e339..79f5c0c1 100644
--- a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs
+++ b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ partial class DataDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("TelegramSearchBot.Model.Data.AccountBook", b =>
{
@@ -300,12 +300,21 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
+ b.Property("AgentChatBatchWindowSeconds")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgentChatMode")
+ .HasColumnType("INTEGER");
+
b.Property("GroupId")
.HasColumnType("INTEGER");
b.Property("ImageGenerationModelName")
.HasColumnType("TEXT");
+ b.Property("IsAgentChatEnabled")
+ .HasColumnType("INTEGER");
+
b.Property("IsManagerGroup")
.HasColumnType("INTEGER");
diff --git a/TelegramSearchBot.Database/Model/Data/GroupSettings.cs b/TelegramSearchBot.Database/Model/Data/GroupSettings.cs
index e4233b63..e2114539 100644
--- a/TelegramSearchBot.Database/Model/Data/GroupSettings.cs
+++ b/TelegramSearchBot.Database/Model/Data/GroupSettings.cs
@@ -8,6 +8,11 @@
namespace TelegramSearchBot.Model.Data {
+ public enum GroupAgentChatMode {
+ GuidedBatch = 0,
+ Sequential = 1
+ }
+
[Index(nameof(GroupId), IsUnique = true)]
public class GroupSettings {
[Key]
@@ -17,6 +22,9 @@ public class GroupSettings {
public string LLMModelName { get; set; }
public string ImageGenerationModelName { get; set; }
public string MusicGenerationModelName { get; set; }
+ public bool IsAgentChatEnabled { get; set; }
+ public GroupAgentChatMode AgentChatMode { get; set; } = GroupAgentChatMode.GuidedBatch;
+ public int? AgentChatBatchWindowSeconds { get; set; }
///
/// 是否是有管理员权限的群,是的所有群友都可以作为管理员操作一部分功能
///
diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/GroupLlmSettingsServiceTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/GroupLlmSettingsServiceTests.cs
new file mode 100644
index 00000000..56101961
--- /dev/null
+++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/GroupLlmSettingsServiceTests.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using TelegramSearchBot.Interface.AI.LLM;
+using TelegramSearchBot.Model;
+using TelegramSearchBot.Model.Data;
+using TelegramSearchBot.Service.AI.LLM;
+using Xunit;
+
+namespace TelegramSearchBot.LLM.Test.Service.AI.LLM {
+ public class GroupLlmSettingsServiceTests {
+ [Fact]
+ public async Task SetAgentChatModeAsync_CreatesGroupSettingsWithDefaultWindow() {
+ await using var dbContext = CreateDbContext();
+ var service = new GroupLlmSettingsService(dbContext);
+
+ var settings = await service.SetAgentChatModeAsync(-1001, true, GroupAgentChatMode.GuidedBatch);
+
+ Assert.True(settings.IsEnabled);
+ Assert.Equal(GroupAgentChatMode.GuidedBatch, settings.Mode);
+ Assert.Equal(GroupAgentChatSettings.DefaultBatchWindowSeconds, settings.BatchWindowSeconds);
+
+ var stored = await dbContext.GroupSettings.SingleAsync(x => x.GroupId == -1001);
+ Assert.True(stored.IsAgentChatEnabled);
+ Assert.Equal(GroupAgentChatMode.GuidedBatch, stored.AgentChatMode);
+ Assert.Equal(GroupAgentChatSettings.DefaultBatchWindowSeconds, stored.AgentChatBatchWindowSeconds);
+ }
+
+ [Fact]
+ public async Task SetAgentChatModeAsync_PreservesExistingModels() {
+ await using var dbContext = CreateDbContext();
+ dbContext.GroupSettings.Add(new GroupSettings {
+ GroupId = -1002,
+ LLMModelName = "gpt-test",
+ ImageGenerationModelName = "gpt-image-test",
+ MusicGenerationModelName = "music-test"
+ });
+ await dbContext.SaveChangesAsync();
+ var service = new GroupLlmSettingsService(dbContext);
+
+ var settings = await service.SetAgentChatModeAsync(-1002, true, GroupAgentChatMode.Sequential, 9);
+
+ Assert.True(settings.IsEnabled);
+ Assert.Equal(GroupAgentChatMode.Sequential, settings.Mode);
+ Assert.Equal(9, settings.BatchWindowSeconds);
+ Assert.Equal("gpt-test", settings.ModelName);
+
+ var stored = await dbContext.GroupSettings.SingleAsync(x => x.GroupId == -1002);
+ Assert.Equal("gpt-test", stored.LLMModelName);
+ Assert.Equal("gpt-image-test", stored.ImageGenerationModelName);
+ Assert.Equal("music-test", stored.MusicGenerationModelName);
+ }
+
+ private static DataDbContext CreateDbContext() {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase($"GroupLlmSettingsServiceTests_{Guid.NewGuid():N}")
+ .Options;
+ return new DataDbContext(options);
+ }
+ }
+}
diff --git a/TelegramSearchBot.LLM/Interface/AI/LLM/IGroupLlmSettingsService.cs b/TelegramSearchBot.LLM/Interface/AI/LLM/IGroupLlmSettingsService.cs
index 51d91810..74c653d1 100644
--- a/TelegramSearchBot.LLM/Interface/AI/LLM/IGroupLlmSettingsService.cs
+++ b/TelegramSearchBot.LLM/Interface/AI/LLM/IGroupLlmSettingsService.cs
@@ -1,11 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
+using TelegramSearchBot.Model.Data;
#nullable enable
namespace TelegramSearchBot.Interface.AI.LLM {
+ public sealed class GroupAgentChatSettings {
+ public const int DefaultBatchWindowSeconds = 5;
+
+ public bool IsEnabled { get; set; }
+ public GroupAgentChatMode Mode { get; set; } = GroupAgentChatMode.GuidedBatch;
+ public int BatchWindowSeconds { get; set; } = DefaultBatchWindowSeconds;
+ public string? ModelName { get; set; }
+ }
+
public interface IGroupLlmSettingsService {
Task GetModelAsync(long chatId, CancellationToken cancellationToken = default);
Task<(string Previous, string Current)> SetModelAsync(long chatId, string modelName, CancellationToken cancellationToken = default);
+ Task GetAgentChatSettingsAsync(long chatId, CancellationToken cancellationToken = default);
+ Task SetAgentChatModeAsync(
+ long chatId,
+ bool isEnabled,
+ GroupAgentChatMode mode,
+ int? batchWindowSeconds = null,
+ CancellationToken cancellationToken = default);
}
}
diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/GroupLlmSettingsService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/GroupLlmSettingsService.cs
index be63a54a..052135f2 100644
--- a/TelegramSearchBot.LLM/Service/AI/LLM/GroupLlmSettingsService.cs
+++ b/TelegramSearchBot.LLM/Service/AI/LLM/GroupLlmSettingsService.cs
@@ -14,6 +14,8 @@
namespace TelegramSearchBot.Service.AI.LLM {
[Injectable(ServiceLifetime.Scoped)]
public class GroupLlmSettingsService : IService, IGroupLlmSettingsService {
+ private const int MinBatchWindowSeconds = 1;
+ private const int MaxBatchWindowSeconds = 60;
private readonly DataDbContext _dbContext;
public GroupLlmSettingsService(DataDbContext dbContext) {
@@ -28,6 +30,22 @@ public GroupLlmSettingsService(DataDbContext dbContext) {
return settings == null ? null : settings.LLMModelName;
}
+ public async Task GetAgentChatSettingsAsync(long chatId, CancellationToken cancellationToken = default) {
+ var settings = await _dbContext.GroupSettings.AsNoTracking()
+ .FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
+
+ if (settings is null) {
+ return new GroupAgentChatSettings();
+ }
+
+ return new GroupAgentChatSettings {
+ IsEnabled = settings.IsAgentChatEnabled,
+ Mode = settings.AgentChatMode,
+ BatchWindowSeconds = NormalizeBatchWindow(settings.AgentChatBatchWindowSeconds),
+ ModelName = settings.LLMModelName
+ };
+ }
+
public async Task<(string Previous, string Current)> SetModelAsync(long chatId, string modelName, CancellationToken cancellationToken = default) {
var normalizedModelName = modelName?.Trim();
if (string.IsNullOrWhiteSpace(normalizedModelName)) {
@@ -67,5 +85,60 @@ public GroupLlmSettingsService(DataDbContext dbContext) {
return (previous ?? "Default", modelName);
}
+
+ public async Task SetAgentChatModeAsync(
+ long chatId,
+ bool isEnabled,
+ GroupAgentChatMode mode,
+ int? batchWindowSeconds = null,
+ CancellationToken cancellationToken = default) {
+ var normalizedWindow = NormalizeBatchWindow(batchWindowSeconds);
+ var settings = await _dbContext.GroupSettings
+ .FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
+ GroupSettings? newSettings = null;
+
+ if (settings is null) {
+ newSettings = new GroupSettings {
+ GroupId = chatId
+ };
+ settings = newSettings;
+ await _dbContext.GroupSettings.AddAsync(settings, cancellationToken);
+ }
+
+ settings.IsAgentChatEnabled = isEnabled;
+ settings.AgentChatMode = mode;
+ settings.AgentChatBatchWindowSeconds = normalizedWindow;
+
+ try {
+ await _dbContext.SaveChangesAsync(cancellationToken);
+ } catch (DbUpdateException) when (newSettings != null) {
+ _dbContext.Entry(newSettings).State = EntityState.Detached;
+ settings = await _dbContext.GroupSettings
+ .FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
+ if (settings is null) {
+ throw;
+ }
+
+ settings.IsAgentChatEnabled = isEnabled;
+ settings.AgentChatMode = mode;
+ settings.AgentChatBatchWindowSeconds = normalizedWindow;
+ await _dbContext.SaveChangesAsync(cancellationToken);
+ }
+
+ return new GroupAgentChatSettings {
+ IsEnabled = settings.IsAgentChatEnabled,
+ Mode = settings.AgentChatMode,
+ BatchWindowSeconds = NormalizeBatchWindow(settings.AgentChatBatchWindowSeconds),
+ ModelName = settings.LLMModelName
+ };
+ }
+
+ private static int NormalizeBatchWindow(int? batchWindowSeconds) {
+ if (!batchWindowSeconds.HasValue || batchWindowSeconds.Value <= 0) {
+ return GroupAgentChatSettings.DefaultBatchWindowSeconds;
+ }
+
+ return Math.Clamp(batchWindowSeconds.Value, MinBatchWindowSeconds, MaxBatchWindowSeconds);
+ }
}
}
diff --git a/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs b/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs
index cbd01b3d..a756ae1f 100644
--- a/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs
+++ b/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs
@@ -16,6 +16,72 @@
namespace TelegramSearchBot.Test.Service.AI.LLM {
[Collection("AgentEnvSerial")]
public class LLMTaskQueueServiceTests {
+ [Fact]
+ public async Task EnqueueMessageTaskAsync_WithCustomInput_PersistsInputMessage() {
+ var originalFlag = Env.EnableLLMAgentProcess;
+ Env.EnableLLMAgentProcess = true;
+
+ try {
+ await using var dbContext = CreateDbContext();
+ SeedChannel(dbContext, 322, "gpt-agent-chat");
+ dbContext.GroupSettings.Add(new GroupSettings {
+ GroupId = -1001,
+ LLMModelName = "gpt-agent-chat"
+ });
+ await dbContext.SaveChangesAsync();
+
+ var redisMock = new Mock();
+ var dbMock = new Mock();
+ redisMock.Setup(r => r.GetDatabase(It.IsAny(), It.IsAny