From 72ec7773e86b7b33ac92b77dded5364069f763da Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Mon, 1 Jun 2026 19:22:47 +0800 Subject: [PATCH 1/4] Add LLM invisibility for group users --- ...LlmInvisibilityToUserWithGroup.Designer.cs | 896 ++++++++++++++++++ ...11353_AddLlmInvisibilityToUserWithGroup.cs | 29 + .../Migrations/DataDbContextModelSnapshot.cs | 3 + .../Model/Data/UserWithGroup.cs | 1 + .../AI/LLM/LlmVisibilityServiceTests.cs | 62 ++ .../Service/AI/LLM/AnthropicService.cs | 15 +- .../Service/AI/LLM/GeminiService.cs | 15 +- .../Service/AI/LLM/LlmVisibilityService.cs | 83 ++ .../Service/AI/LLM/OpenAIResponsesService.cs | 15 +- .../Service/AI/LLM/OpenAIService.cs | 16 +- .../AI/LLM/LLMTaskQueueServiceTests.cs | 107 +++ .../AI/LLM/AgentChatModeController.cs | 13 + .../Controller/AI/LLM/AltPhotoController.cs | 11 +- .../Controller/AI/LLM/GeneralLLMController.cs | 12 +- .../Manage/LlmVisibilityController.cs | 71 ++ .../AI/LLM/AgentChatBatchDispatchService.cs | 31 +- .../Service/AI/LLM/LLMTaskQueueService.cs | 9 +- .../Service/Manage/RefreshService.cs | 11 +- .../Service/Tools/SearchToolService.cs | 68 +- 19 files changed, 1444 insertions(+), 24 deletions(-) create mode 100644 TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.Designer.cs create mode 100644 TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.cs create mode 100644 TelegramSearchBot.LLM.Test/Service/AI/LLM/LlmVisibilityServiceTests.cs create mode 100644 TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs create mode 100644 TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs diff --git a/TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.Designer.cs b/TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.Designer.cs new file mode 100644 index 00000000..b75ae702 --- /dev/null +++ b/TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.Designer.cs @@ -0,0 +1,896 @@ +// +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("20260601111353_AddLlmInvisibilityToUserWithGroup")] + partial class AddLlmInvisibilityToUserWithGroup + { + /// + 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("IsLlmInvisible") + .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/20260601111353_AddLlmInvisibilityToUserWithGroup.cs b/TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.cs new file mode 100644 index 00000000..d66a09bf --- /dev/null +++ b/TelegramSearchBot.Database/Migrations/20260601111353_AddLlmInvisibilityToUserWithGroup.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TelegramSearchBot.Migrations +{ + /// + public partial class AddLlmInvisibilityToUserWithGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsLlmInvisible", + table: "UsersWithGroup", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsLlmInvisible", + table: "UsersWithGroup"); + } + } +} diff --git a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs index 79f5c0c1..fab6b8bc 100644 --- a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs +++ b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs @@ -730,6 +730,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("GroupId") .HasColumnType("INTEGER"); + b.Property("IsLlmInvisible") + .HasColumnType("INTEGER"); + b.Property("UserId") .HasColumnType("INTEGER"); diff --git a/TelegramSearchBot.Database/Model/Data/UserWithGroup.cs b/TelegramSearchBot.Database/Model/Data/UserWithGroup.cs index 325cf7ea..8853f352 100644 --- a/TelegramSearchBot.Database/Model/Data/UserWithGroup.cs +++ b/TelegramSearchBot.Database/Model/Data/UserWithGroup.cs @@ -11,5 +11,6 @@ public class UserWithGroup { public long Id { get; set; } public long GroupId { get; set; } public long UserId { get; set; } + public bool IsLlmInvisible { get; set; } } } diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/LlmVisibilityServiceTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/LlmVisibilityServiceTests.cs new file mode 100644 index 00000000..1aaac2cd --- /dev/null +++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/LlmVisibilityServiceTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TelegramSearchBot.Model; +using TelegramSearchBot.Model.Data; +using TelegramSearchBot.Service.AI.LLM; +using Xunit; + +namespace TelegramSearchBot.LLM.Test.Service.AI.LLM { + public class LlmVisibilityServiceTests { + [Fact] + public async Task SetUserInvisibleAsync_UpdatesGroupScopedState() { + await using var dbContext = CreateDbContext(); + var service = new LlmVisibilityService(dbContext); + + var enabled = await service.SetUserInvisibleAsync(-1001, 42, true); + var isInvisible = await service.IsUserInvisibleAsync(-1001, 42); + var isInvisibleInOtherGroup = await service.IsUserInvisibleAsync(-1002, 42); + + Assert.True(enabled); + Assert.True(isInvisible); + Assert.False(isInvisibleInOtherGroup); + + var disabled = await service.SetUserInvisibleAsync(-1001, 42, false); + + Assert.False(disabled); + Assert.False(await service.IsUserInvisibleAsync(-1001, 42)); + } + + [Fact] + public async Task FilterVisibleMessagesAsync_RemovesInvisibleUsersMessages() { + await using var dbContext = CreateDbContext(); + dbContext.UsersWithGroup.Add(new UserWithGroup { + GroupId = -1001, + UserId = 42, + IsLlmInvisible = true + }); + await dbContext.SaveChangesAsync(); + + var service = new LlmVisibilityService(dbContext); + var messages = new List { + new() { GroupId = -1001, FromUserId = 10, Content = "visible" }, + new() { GroupId = -1001, FromUserId = 42, Content = "hidden" }, + new() { GroupId = -1001, FromUserId = 11, Content = "also visible" } + }; + + var filtered = await service.FilterVisibleMessagesAsync(-1001, messages); + + Assert.Equal(new long[] { 10, 11 }, filtered.Select(x => x.FromUserId).ToArray()); + Assert.DoesNotContain(filtered, x => x.Content == "hidden"); + } + + private static DataDbContext CreateDbContext() { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"LlmVisibilityServiceTests_{Guid.NewGuid():N}") + .Options; + return new DataDbContext(options); + } + } +} diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs index 20d64539..6ea63e55 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs @@ -33,6 +33,7 @@ public class AnthropicService : IService, ILLMService { private readonly IMessageExtensionService _messageExtensionService; private readonly IHttpClientFactory _httpClientFactory; private readonly IBotIdentityProvider _botIdentityProvider; + private readonly LlmVisibilityService _llmVisibilityService; private string _fallbackBotName = string.Empty; public string BotName { @@ -61,7 +62,7 @@ public AnthropicService( ILogger logger, IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory) - : this(context, logger, messageExtensionService, httpClientFactory, null) { + : this(context, logger, messageExtensionService, httpClientFactory, null, null) { } public AnthropicService( @@ -69,12 +70,14 @@ public AnthropicService( ILogger logger, IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory, - IBotIdentityProvider botIdentityProvider) { + IBotIdentityProvider botIdentityProvider, + LlmVisibilityService llmVisibilityService = null) { _logger = logger; _dbContext = context; _messageExtensionService = messageExtensionService; _httpClientFactory = httpClientFactory; _botIdentityProvider = botIdentityProvider; + _llmVisibilityService = llmVisibilityService; _logger.LogInformation("AnthropicService instance created. McpToolHelper should be initialized at application startup."); } @@ -181,7 +184,13 @@ public bool IsSameSender(DataMessage message1, DataMessage message2) { .ToListAsync(); } - if (inputMessage != null) { + if (_llmVisibilityService != null) { + dbMessages = await _llmVisibilityService.FilterVisibleMessagesAsync(chatId, dbMessages); + } + + if (inputMessage != null && + ( _llmVisibilityService == null || + !await _llmVisibilityService.IsUserInvisibleAsync(chatId, inputMessage.FromUserId) )) { dbMessages.Add(inputMessage); } diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs index 75339594..eebd5de7 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs @@ -30,6 +30,7 @@ public class GeminiService : ILLMService, IService { private readonly Dictionary _chatSessions = new(); private readonly IHttpClientFactory _httpClientFactory; private readonly IBotIdentityProvider _botIdentityProvider; + private readonly LlmVisibilityService _llmVisibilityService; private string _fallbackBotName = string.Empty; public string BotName { get => GetBotNameAsync().ConfigureAwait(false).GetAwaiter().GetResult(); @@ -46,18 +47,20 @@ public GeminiService( DataDbContext context, ILogger logger, IHttpClientFactory httpClientFactory) - : this(context, logger, httpClientFactory, null) { + : this(context, logger, httpClientFactory, null, null) { } public GeminiService( DataDbContext context, ILogger logger, IHttpClientFactory httpClientFactory, - IBotIdentityProvider botIdentityProvider) { + IBotIdentityProvider botIdentityProvider, + LlmVisibilityService llmVisibilityService = null) { _logger = logger; _dbContext = context; _httpClientFactory = httpClientFactory; _botIdentityProvider = botIdentityProvider; + _llmVisibilityService = llmVisibilityService; _logger.LogInformation("GeminiService instance created"); } @@ -120,7 +123,13 @@ private void AddMessageToHistory(List chatHistory, l .ToListAsync(); } - if (inputMessage != null) { + if (_llmVisibilityService != null) { + messages = await _llmVisibilityService.FilterVisibleMessagesAsync(chatId, messages); + } + + if (inputMessage != null && + ( _llmVisibilityService == null || + !await _llmVisibilityService.IsUserInvisibleAsync(chatId, inputMessage.FromUserId) )) { messages.Add(inputMessage); } diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs new file mode 100644 index 00000000..cac19536 --- /dev/null +++ b/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TelegramSearchBot.Attributes; +using TelegramSearchBot.Interface; +using TelegramSearchBot.Model; +using TelegramSearchBot.Model.Data; + +namespace TelegramSearchBot.Service.AI.LLM { + [Injectable(ServiceLifetime.Transient)] + public class LlmVisibilityService : IService { + private readonly DataDbContext _dbContext; + + public LlmVisibilityService(DataDbContext dbContext) { + _dbContext = dbContext; + } + + public string ServiceName => "LlmVisibilityService"; + + public async Task IsUserInvisibleAsync(long groupId, long userId, CancellationToken cancellationToken = default) { + if (groupId == 0 || userId == 0) return false; + + return await _dbContext.UsersWithGroup.AsNoTracking() + .AnyAsync(x => x.GroupId == groupId && + x.UserId == userId && + x.IsLlmInvisible, + cancellationToken); + } + + public async Task> GetInvisibleUserIdsAsync(long groupId, CancellationToken cancellationToken = default) { + if (groupId == 0) return new HashSet(); + + var userIds = await _dbContext.UsersWithGroup.AsNoTracking() + .Where(x => x.GroupId == groupId && x.IsLlmInvisible) + .Select(x => x.UserId) + .ToListAsync(cancellationToken); + + return userIds.ToHashSet(); + } + + public async Task> FilterVisibleMessagesAsync( + long groupId, + IEnumerable messages, + CancellationToken cancellationToken = default) { + var messageList = messages?.ToList() ?? new List(); + if (messageList.Count == 0) return messageList; + + var invisibleUserIds = await GetInvisibleUserIdsAsync(groupId, cancellationToken); + if (invisibleUserIds.Count == 0) return messageList; + + return messageList + .Where(message => !invisibleUserIds.Contains(message.FromUserId)) + .ToList(); + } + + public async Task SetUserInvisibleAsync( + long groupId, + long userId, + bool isInvisible, + CancellationToken cancellationToken = default) { + var userWithGroup = await _dbContext.UsersWithGroup + .FirstOrDefaultAsync(x => x.GroupId == groupId && x.UserId == userId, cancellationToken); + + if (userWithGroup == null) { + userWithGroup = new UserWithGroup { + GroupId = groupId, + UserId = userId, + IsLlmInvisible = isInvisible + }; + await _dbContext.UsersWithGroup.AddAsync(userWithGroup, cancellationToken); + } else { + userWithGroup.IsLlmInvisible = isInvisible; + _dbContext.UsersWithGroup.Update(userWithGroup); + } + + await _dbContext.SaveChangesAsync(cancellationToken); + return userWithGroup.IsLlmInvisible; + } + } +} diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIResponsesService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIResponsesService.cs index 31c8ef6d..a4b592c6 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIResponsesService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIResponsesService.cs @@ -64,13 +64,14 @@ public string BotName { private readonly DataDbContext _dbContext; private readonly IHttpClientFactory _httpClientFactory; private readonly IMessageExtensionService _messageExtensionService; + private readonly LlmVisibilityService _llmVisibilityService; public OpenAIResponsesService( DataDbContext context, ILogger logger, IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory) - : this(context, logger, messageExtensionService, httpClientFactory, null) { + : this(context, logger, messageExtensionService, httpClientFactory, null, null) { } public OpenAIResponsesService( @@ -78,12 +79,14 @@ public OpenAIResponsesService( ILogger logger, IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory, - IBotIdentityProvider botIdentityProvider) { + IBotIdentityProvider botIdentityProvider, + LlmVisibilityService llmVisibilityService = null) { _logger = logger; _dbContext = context; _messageExtensionService = messageExtensionService; _httpClientFactory = httpClientFactory; _botIdentityProvider = botIdentityProvider; + _llmVisibilityService = llmVisibilityService; _logger.LogInformation("OpenAIResponsesService instance created."); } @@ -785,7 +788,13 @@ private async Task> BuildResponseInputItemsAsync( .ToListAsync(); } - if (inputToken != null) { + if (_llmVisibilityService != null) { + messages = await _llmVisibilityService.FilterVisibleMessagesAsync(ChatId, messages); + } + + if (inputToken != null && + ( _llmVisibilityService == null || + !await _llmVisibilityService.IsUserInvisibleAsync(ChatId, inputToken.FromUserId) )) { messages.Add(inputToken); } diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs index a6229d43..7e24fca1 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs @@ -112,6 +112,7 @@ private static string SanitizeAndTruncateArguments(string arguments, int maxChar private readonly IMessageExtensionService _messageExtensionService; private readonly IBotIdentityProvider _botIdentityProvider; private readonly IGroupLlmSettingsService _groupLlmSettingsService; + private readonly LlmVisibilityService _llmVisibilityService; private string _fallbackBotName = string.Empty; public OpenAIService( @@ -119,7 +120,7 @@ public OpenAIService( ILogger logger, IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory) - : this(context, logger, messageExtensionService, httpClientFactory, null, null) { + : this(context, logger, messageExtensionService, httpClientFactory, null, null, null) { } public OpenAIService( @@ -128,13 +129,15 @@ public OpenAIService( IMessageExtensionService messageExtensionService, IHttpClientFactory httpClientFactory, IBotIdentityProvider botIdentityProvider, - IGroupLlmSettingsService groupLlmSettingsService) { + IGroupLlmSettingsService groupLlmSettingsService, + LlmVisibilityService llmVisibilityService = null) { _logger = logger; _dbContext = context; _messageExtensionService = messageExtensionService; _httpClientFactory = httpClientFactory; _botIdentityProvider = botIdentityProvider; _groupLlmSettingsService = groupLlmSettingsService; + _llmVisibilityService = llmVisibilityService; _logger.LogInformation("OpenAIService instance created. McpToolHelper should be initialized at application startup."); } @@ -836,7 +839,14 @@ public async Task> GetChatHistory(long ChatId, List m.DateTime) .ToListAsync(); } - if (InputToken != null) { + + if (_llmVisibilityService != null) { + Messages = await _llmVisibilityService.FilterVisibleMessagesAsync(ChatId, Messages); + } + + if (InputToken != null && + ( _llmVisibilityService == null || + !await _llmVisibilityService.IsUserInvisibleAsync(ChatId, InputToken.FromUserId) )) { Messages.Add(InputToken); } _logger.LogInformation($"OpenAI GetChatHistory: Found {Messages.Count} messages for ChatId {ChatId}."); diff --git a/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs b/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs index a756ae1f..be95cac2 100644 --- a/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs +++ b/TelegramSearchBot.Test/Service/AI/LLM/LLMTaskQueueServiceTests.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; +using Newtonsoft.Json; using StackExchange.Redis; using TelegramSearchBot.Common; using TelegramSearchBot.Model; @@ -82,6 +83,112 @@ public async Task EnqueueMessageTaskAsync_WithCustomInput_PersistsInputMessage() } } + [Fact] + public async Task EnqueueMessageTaskAsync_FiltersInvisibleUsersFromHistoryAndExtensions() { + var originalFlag = Env.EnableLLMAgentProcess; + Env.EnableLLMAgentProcess = true; + + try { + await using var dbContext = CreateDbContext(); + SeedChannel(dbContext, 323, "gpt-agent-chat"); + dbContext.GroupSettings.Add(new GroupSettings { + GroupId = -1001, + LLMModelName = "gpt-agent-chat" + }); + dbContext.UsersWithGroup.Add(new UserWithGroup { + GroupId = -1001, + UserId = 222, + IsLlmInvisible = true + }); + + var visibleMessage = new Message { + GroupId = -1001, + MessageId = 1, + FromUserId = 111, + Content = "visible context", + DateTime = DateTime.UtcNow.AddMinutes(-2) + }; + var hiddenMessage = new Message { + GroupId = -1001, + MessageId = 2, + FromUserId = 222, + Content = "hidden context", + DateTime = DateTime.UtcNow.AddMinutes(-1) + }; + dbContext.Messages.AddRange(visibleMessage, hiddenMessage); + await dbContext.SaveChangesAsync(); + + dbContext.MessageExtensions.AddRange( + new MessageExtension { + MessageDataId = visibleMessage.Id, + Name = "OCR_Result", + Value = "visible ocr" + }, + new MessageExtension { + MessageDataId = hiddenMessage.Id, + Name = "Alt_Result", + Value = "hidden alt" + }); + await dbContext.SaveChangesAsync(); + + var redisMock = new Mock(); + var dbMock = new Mock(); + redisMock.Setup(r => r.GetDatabase(It.IsAny(), It.IsAny())).Returns(dbMock.Object); + + dbMock.Setup(d => d.HashGetAllAsync( + It.Is(key => key == LlmAgentRedisKeys.AgentSession(-1001)), + It.IsAny())) + .ReturnsAsync([ + new HashEntry("chatId", -1001), + new HashEntry("processId", 999), + new HashEntry("port", 0), + new HashEntry("status", "idle"), + new HashEntry("lastHeartbeatUtc", DateTime.UtcNow.ToString("O")), + new HashEntry("lastActiveAtUtc", DateTime.UtcNow.ToString("O")) + ]); + + string pushedPayload = string.Empty; + dbMock.Setup(d => d.ListLeftPushAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback((_, value, _, _) => pushedPayload = value.ToString()) + .ReturnsAsync(1); + dbMock.Setup(d => d.HashSetAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var registry = new AgentRegistryService( + redisMock.Object, + Mock.Of(), + Mock.Of>()); + var polling = new ChunkPollingService(redisMock.Object); + var visibility = new LlmVisibilityService(dbContext); + var service = new LLMTaskQueueService(dbContext, redisMock.Object, polling, registry, visibility); + + await service.EnqueueMessageTaskAsync( + -1001, + 111, + 789, + DateTime.UtcNow, + "current input", + "bot", + 1001); + + var task = JsonConvert.DeserializeObject(pushedPayload); + + Assert.NotNull(task); + Assert.Single(task!.History); + Assert.Equal("visible context", task.History[0].Content); + Assert.Equal(111, task.History[0].FromUserId); + Assert.Contains(task.History[0].Extensions, x => x.Name == "OCR_Result" && x.Value == "visible ocr"); + Assert.DoesNotContain(task.History, x => x.FromUserId == 222); + Assert.DoesNotContain(task.History.SelectMany(x => x.Extensions), x => x.Value == "hidden alt"); + } finally { + Env.EnableLLMAgentProcess = originalFlag; + } + } + [Fact] public async Task EnqueueContinuationTaskAsync_PersistsPayloadAndRecoveryMetadata() { var originalFlag = Env.EnableLLMAgentProcess; diff --git a/TelegramSearchBot/Controller/AI/LLM/AgentChatModeController.cs b/TelegramSearchBot/Controller/AI/LLM/AgentChatModeController.cs index e12e9713..61ecaaa9 100644 --- a/TelegramSearchBot/Controller/AI/LLM/AgentChatModeController.cs +++ b/TelegramSearchBot/Controller/AI/LLM/AgentChatModeController.cs @@ -33,6 +33,7 @@ public sealed class AgentChatModeController : IOnUpdate { private readonly AgentChatExecutionService _executionService; private readonly AgentChatBatchQueueService _batchQueueService; private readonly IConnectionMultiplexer _redis; + private readonly LlmVisibilityService _llmVisibilityService; private readonly ILogger _logger; public AgentChatModeController( @@ -44,6 +45,7 @@ public AgentChatModeController( AgentChatExecutionService executionService, AgentChatBatchQueueService batchQueueService, IConnectionMultiplexer redis, + LlmVisibilityService llmVisibilityService, ILogger logger) { _groupLlmSettingsService = groupLlmSettingsService; _botIdentityProvider = botIdentityProvider; @@ -53,6 +55,7 @@ public AgentChatModeController( _executionService = executionService; _batchQueueService = batchQueueService; _redis = redis; + _llmVisibilityService = llmVisibilityService; _logger = logger; } @@ -84,6 +87,16 @@ public async Task ExecuteAsync(PipelineContext p) { return; } + var senderUserId = telegramMessage.From?.Id ?? 0; + if (senderUserId != 0 && await _llmVisibilityService.IsUserInvisibleAsync(telegramMessage.Chat.Id, senderUserId)) { + _logger.LogInformation( + "User {UserId} in chat {ChatId} is LLM invisible; skipping agent chat for message {MessageId}.", + senderUserId, + telegramMessage.Chat.Id, + telegramMessage.MessageId); + return; + } + var botIdentity = await EnsureBotIdentityAsync(); if (ShouldIgnoreMessage(telegramMessage, messageText, botIdentity)) { return; diff --git a/TelegramSearchBot/Controller/AI/LLM/AltPhotoController.cs b/TelegramSearchBot/Controller/AI/LLM/AltPhotoController.cs index ca735db6..e6779ab9 100644 --- a/TelegramSearchBot/Controller/AI/LLM/AltPhotoController.cs +++ b/TelegramSearchBot/Controller/AI/LLM/AltPhotoController.cs @@ -31,6 +31,7 @@ public class AltPhotoController : IOnUpdate { private readonly ILogger logger; private readonly ISendMessageService SendMessageService; private readonly MessageExtensionService MessageExtensionService; + private readonly LlmVisibilityService LlmVisibilityService; public AltPhotoController( ITelegramBotClient botClient, IGeneralLLMService generalLLMService, @@ -38,7 +39,8 @@ public AltPhotoController( MessageService messageService, ILogger logger, ISendMessageService sendMessageService, - MessageExtensionService messageExtensionService + MessageExtensionService messageExtensionService, + LlmVisibilityService llmVisibilityService ) { this.generalLLMService = generalLLMService; this.messageService = messageService; @@ -47,6 +49,7 @@ MessageExtensionService messageExtensionService this.logger = logger; SendMessageService = sendMessageService; MessageExtensionService = messageExtensionService; + LlmVisibilityService = llmVisibilityService; } public async Task ExecuteAsync(PipelineContext p) { var e = p.Update; @@ -60,6 +63,12 @@ public async Task ExecuteAsync(PipelineContext p) { try { var PhotoPath = IProcessPhoto.GetPhotoPath(e); logger.LogInformation($"Get Photo File: {e.Message.Chat.Id}/{e.Message.MessageId}"); + if (e.Message.From != null && + await LlmVisibilityService.IsUserInvisibleAsync(e.Message.Chat.Id, e.Message.From.Id)) { + logger.LogInformation("Skip Alt generation for LLM invisible user {UserId} in chat {ChatId}, message {MessageId}", + e.Message.From.Id, e.Message.Chat.Id, e.Message.MessageId); + return; + } OcrStr = await generalLLMService.AnalyzeImageAsync(PhotoPath, e.Message.Chat.Id); using (LoggerHolders.PushChatContentLogScope()) { logger.LogInformation(OcrStr); diff --git a/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs b/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs index 49a4f33e..95764e61 100644 --- a/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs +++ b/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs @@ -33,6 +33,7 @@ public class GeneralLLMController : IOnUpdate { private readonly MusicGenerationToolSettingsService _musicGenerationToolSettingsService; private readonly IModelCapabilityService _modelCapabilityService; private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly LlmVisibilityService _llmVisibilityService; public List Dependencies => new List(); public ITelegramBotClient botClient { get; set; } public MessageService messageService { get; set; } @@ -56,7 +57,8 @@ public GeneralLLMController( ImageGenerationToolSettingsService imageGenerationToolSettingsService, MusicGenerationToolSettingsService musicGenerationToolSettingsService, IModelCapabilityService modelCapabilityService, - IConnectionMultiplexer connectionMultiplexer + IConnectionMultiplexer connectionMultiplexer, + LlmVisibilityService llmVisibilityService ) { this.logger = logger; this.botClient = botClient; @@ -73,6 +75,7 @@ IConnectionMultiplexer connectionMultiplexer _musicGenerationToolSettingsService = musicGenerationToolSettingsService; _modelCapabilityService = modelCapabilityService; _connectionMultiplexer = connectionMultiplexer; + _llmVisibilityService = llmVisibilityService; } public async Task ExecuteAsync(PipelineContext p) { @@ -242,6 +245,13 @@ public async Task ExecuteAsync(PipelineContext p) { bool isReplyToBot = telegramMessage.ReplyToMessage != null && telegramMessage.ReplyToMessage.From != null && telegramMessage.ReplyToMessage.From.Id == botIdentity.UserId; if (isMentionToBot || isReplyToBot) { + if (fromUserId != 0 && await _llmVisibilityService.IsUserInvisibleAsync(telegramMessage.Chat.Id, fromUserId)) { + logger.LogInformation("User {UserId} in chat {ChatId} is LLM invisible; skipping LLM execution for message {MessageId}.", + fromUserId, telegramMessage.Chat.Id, telegramMessage.MessageId); + await SendMessageService.SendMessage("你已开启 LLM 隐身,这条消息不会发送给 LLM。发送 `取消LLM隐身` 可恢复。", telegramMessage.Chat.Id, telegramMessage.MessageId); + return; + } + var modelName = await _groupLlmSettingsService.GetModelAsync(telegramMessage.Chat.Id); if (string.IsNullOrWhiteSpace(modelName)) { logger.LogWarning("请指定模型名称"); diff --git a/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs new file mode 100644 index 00000000..78b1e7e6 --- /dev/null +++ b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Telegram.Bot.Types; +using TelegramSearchBot.Interface; +using TelegramSearchBot.Interface.Controller; +using TelegramSearchBot.Model; +using TelegramSearchBot.Service.AI.LLM; +using TelegramSearchBot.Controller.Storage; + +namespace TelegramSearchBot.Controller.Manage { + public class LlmVisibilityController : IOnUpdate { + private readonly LlmVisibilityService _llmVisibilityService; + private readonly ISendMessageService _sendMessageService; + + public LlmVisibilityController( + LlmVisibilityService llmVisibilityService, + ISendMessageService sendMessageService) { + _llmVisibilityService = llmVisibilityService; + _sendMessageService = sendMessageService; + } + + public List Dependencies => new List { typeof(MessageController) }; + + public async Task ExecuteAsync(PipelineContext p) { + var message = p.Update.Message; + if (message == null || message.Chat.Id >= 0 || message.From == null) { + return; + } + + var command = ( message.Text ?? message.Caption ?? string.Empty ).Trim(); + if (string.IsNullOrWhiteSpace(command)) { + return; + } + + if (IsEnableCommand(command)) { + await _llmVisibilityService.SetUserInvisibleAsync(message.Chat.Id, message.From.Id, true); + await _sendMessageService.SendMessage("已开启 LLM 隐身。之后你的群消息、图片 Alt 和消息扩展信息都不会发送给 LLM。", message.Chat.Id, message.MessageId); + return; + } + + if (IsDisableCommand(command)) { + await _llmVisibilityService.SetUserInvisibleAsync(message.Chat.Id, message.From.Id, false); + await _sendMessageService.SendMessage("已关闭 LLM 隐身。之后你的群消息可以进入 LLM 上下文。", message.Chat.Id, message.MessageId); + return; + } + + if (IsStatusCommand(command)) { + var enabled = await _llmVisibilityService.IsUserInvisibleAsync(message.Chat.Id, message.From.Id); + var status = enabled ? "已开启" : "未开启"; + await _sendMessageService.SendMessage($"LLM 隐身状态:{status}", message.Chat.Id, message.MessageId); + } + } + + private static bool IsEnableCommand(string command) { + return command.Equals("LLM隐身", StringComparison.OrdinalIgnoreCase) || + command.Equals("/llm_invisible_on", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsDisableCommand(string command) { + return command.Equals("取消LLM隐身", StringComparison.OrdinalIgnoreCase) || + command.Equals("LLM显身", StringComparison.OrdinalIgnoreCase) || + command.Equals("/llm_invisible_off", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsStatusCommand(string command) { + return command.Equals("LLM隐身状态", StringComparison.OrdinalIgnoreCase) || + command.Equals("/llm_invisible_status", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/TelegramSearchBot/Service/AI/LLM/AgentChatBatchDispatchService.cs b/TelegramSearchBot/Service/AI/LLM/AgentChatBatchDispatchService.cs index 5ad3cc8a..d4cf3347 100644 --- a/TelegramSearchBot/Service/AI/LLM/AgentChatBatchDispatchService.cs +++ b/TelegramSearchBot/Service/AI/LLM/AgentChatBatchDispatchService.cs @@ -118,11 +118,23 @@ private async Task TryDispatchChatBatchAsync(IDatabase db, long chatId, Cancella return; } - var last = bufferedMessages[^1]; + var llmVisibilityService = scope.ServiceProvider.GetRequiredService(); + var visibleBufferedMessages = await FilterVisibleBufferedMessagesAsync( + llmVisibilityService, + chatId, + bufferedMessages, + cancellationToken); + if (visibleBufferedMessages.Count == 0) { + _logger.LogInformation("Dropping agent chat batch after LLM invisibility filtering. ChatId={ChatId}", chatId); + await db.KeyDeleteAsync(LlmAgentRedisKeys.AgentChatBatchMeta(chatId)); + return; + } + + var last = visibleBufferedMessages[^1]; var executionService = scope.ServiceProvider.GetRequiredService(); await executionService.ExecuteAsync(new AgentChatExecutionRequest { ReplyTarget = last.Message, - InputMessage = BuildBatchInput(bufferedMessages), + InputMessage = BuildBatchInput(visibleBufferedMessages), BotName = last.BotName, BotUserId = last.BotUserId, ModelName = settings.ModelName ?? string.Empty, @@ -191,6 +203,21 @@ private static async Task> DrainBufferedMessagesA .ToList(); } + private static async Task> FilterVisibleBufferedMessagesAsync( + LlmVisibilityService llmVisibilityService, + long chatId, + List bufferedMessages, + CancellationToken cancellationToken) { + var invisibleUserIds = await llmVisibilityService.GetInvisibleUserIdsAsync(chatId, cancellationToken); + if (invisibleUserIds.Count == 0) { + return bufferedMessages; + } + + return bufferedMessages + .Where(x => !invisibleUserIds.Contains(x.Message.UserId)) + .ToList(); + } + private static string BuildBatchInput(IReadOnlyList bufferedMessages) { var sb = new StringBuilder(); sb.AppendLine("以下是群内短时间连续发送的多条消息,请作为同一次 Agent 输入处理。"); diff --git a/TelegramSearchBot/Service/AI/LLM/LLMTaskQueueService.cs b/TelegramSearchBot/Service/AI/LLM/LLMTaskQueueService.cs index c14a64a2..f2c00a0d 100644 --- a/TelegramSearchBot/Service/AI/LLM/LLMTaskQueueService.cs +++ b/TelegramSearchBot/Service/AI/LLM/LLMTaskQueueService.cs @@ -22,16 +22,19 @@ public class LLMTaskQueueService : IService { private readonly IConnectionMultiplexer _redis; private readonly ChunkPollingService _chunkPollingService; private readonly AgentRegistryService _agentRegistryService; + private readonly LlmVisibilityService _llmVisibilityService; public LLMTaskQueueService( DataDbContext dbContext, IConnectionMultiplexer redis, ChunkPollingService chunkPollingService, - AgentRegistryService agentRegistryService) { + AgentRegistryService agentRegistryService, + LlmVisibilityService llmVisibilityService = null) { _dbContext = dbContext; _redis = redis; _chunkPollingService = chunkPollingService; _agentRegistryService = agentRegistryService; + _llmVisibilityService = llmVisibilityService; } public string ServiceName => nameof(LLMTaskQueueService); @@ -264,6 +267,10 @@ private async Task> LoadHistoryAsync(long chatId, Canc .ToListAsync(cancellationToken); } + if (_llmVisibilityService != null) { + history = await _llmVisibilityService.FilterVisibleMessagesAsync(chatId, history, cancellationToken); + } + var userIds = history.Select(x => x.FromUserId).Distinct().ToList(); var users = await _dbContext.UserData.AsNoTracking() .Where(x => userIds.Contains(x.Id)) diff --git a/TelegramSearchBot/Service/Manage/RefreshService.cs b/TelegramSearchBot/Service/Manage/RefreshService.cs index 16a88a99..52dd6854 100644 --- a/TelegramSearchBot/Service/Manage/RefreshService.cs +++ b/TelegramSearchBot/Service/Manage/RefreshService.cs @@ -40,6 +40,7 @@ public class RefreshService : MessageService, IService { private readonly IGeneralLLMService _generalLLMService; private readonly FaissVectorService _faissVectorService; private readonly ConversationSegmentationService _conversationSegmentationService; + private readonly LlmVisibilityService _llmVisibilityService; public RefreshService(ILogger logger, LuceneManager lucene, @@ -53,7 +54,8 @@ public RefreshService(ILogger logger, IGeneralLLMService generalLLMService, IMediator mediator, FaissVectorService faissVectorService, - ConversationSegmentationService conversationSegmentationService) : base(logger, lucene, Send, context, mediator) { + ConversationSegmentationService conversationSegmentationService, + LlmVisibilityService llmVisibilityService) : base(logger, lucene, Send, context, mediator) { _logger = logger; _chatImport = chatImport; _autoASRService = autoASRService; @@ -63,6 +65,7 @@ public RefreshService(ILogger logger, _generalLLMService = generalLLMService; _faissVectorService = faissVectorService; _conversationSegmentationService = conversationSegmentationService; + _llmVisibilityService = llmVisibilityService; } private async Task RebuildIndex() { @@ -322,6 +325,12 @@ private async Task ScanAndProcessAltImageFiles() { var messageDataId = await _messageExtensionService.GetMessageIdByMessageIdAndGroupId(messageId, chatId); if (messageDataId.HasValue) { var extensions = await _messageExtensionService.GetByMessageDataIdAsync(messageDataId.Value); + var message = await DataContext.Messages.AsNoTracking() + .FirstOrDefaultAsync(m => m.Id == messageDataId.Value); + if (message != null && + await _llmVisibilityService.IsUserInvisibleAsync(chatId, message.FromUserId)) { + continue; + } // 处理Alt信息 if (!extensions.Any(x => x.Name == "Alt_Result")) { diff --git a/TelegramSearchBot/Service/Tools/SearchToolService.cs b/TelegramSearchBot/Service/Tools/SearchToolService.cs index 793b8f54..2ab88428 100644 --- a/TelegramSearchBot/Service/Tools/SearchToolService.cs +++ b/TelegramSearchBot/Service/Tools/SearchToolService.cs @@ -24,11 +24,17 @@ public class SearchToolService : IService, ISearchToolService { private readonly LuceneManager _luceneManager; private readonly DataDbContext _dbContext; private readonly MessageExtensionService _messageExtensionService; + private readonly LlmVisibilityService _llmVisibilityService; - public SearchToolService(LuceneManager luceneManager, DataDbContext dbContext, MessageExtensionService messageExtensionService) { + public SearchToolService( + LuceneManager luceneManager, + DataDbContext dbContext, + MessageExtensionService messageExtensionService, + LlmVisibilityService llmVisibilityService = null) { _luceneManager = luceneManager; _dbContext = dbContext; _messageExtensionService = messageExtensionService; + _llmVisibilityService = llmVisibilityService; } [BuiltInTool("Searches indexed messages within the current chat using keywords. Supports pagination.")] @@ -46,9 +52,14 @@ public async Task SearchMessagesInCurrentChatAsync( int skip = ( page - 1 ) * pageSize; int take = pageSize; + var invisibleUserIds = await GetInvisibleUserIdsAsync(chatId); (int totalHits, List messageDtos) searchResult; try { - searchResult = _luceneManager.Search(query, chatId, skip, take); + if (invisibleUserIds.Count == 0) { + searchResult = _luceneManager.Search(query, chatId, skip, take); + } else { + searchResult = SearchVisibleLuceneMessages(query, chatId, skip, take, invisibleUserIds); + } } catch (System.IO.DirectoryNotFoundException) { return new SearchToolResult { Query = query, TotalFound = 0, CurrentPage = page, PageSize = pageSize, Results = new List(), Note = $"No search index found for this chat. Messages may not have been indexed yet." }; } catch (Exception ex) { @@ -62,14 +73,18 @@ public async Task SearchMessagesInCurrentChatAsync( // Get context messages var messagesBefore = await _dbContext.Messages .Include(m => m.MessageExtensions) - .Where(m => m.GroupId == chatId && m.DateTime < msg.DateTime) + .Where(m => m.GroupId == chatId && + m.DateTime < msg.DateTime && + !invisibleUserIds.Contains(m.FromUserId)) .OrderByDescending(m => m.DateTime) .Take(5) .ToListAsync(); var messagesAfter = await _dbContext.Messages .Include(m => m.MessageExtensions) - .Where(m => m.GroupId == chatId && m.DateTime > msg.DateTime) + .Where(m => m.GroupId == chatId && + m.DateTime > msg.DateTime && + !invisibleUserIds.Contains(m.FromUserId)) .OrderBy(m => m.DateTime) .Take(5) .ToListAsync(); @@ -158,9 +173,14 @@ public async Task QueryMessageHistory( } try { + var invisibleUserIds = await GetInvisibleUserIdsAsync(chatId); var query = _dbContext.Messages.AsNoTracking() .Where(m => m.GroupId == chatId); + if (invisibleUserIds.Count > 0) { + query = query.Where(m => !invisibleUserIds.Contains(m.FromUserId)); + } + if (!string.IsNullOrWhiteSpace(queryText)) { query = query.Where(m => m.Content != null && m.Content.Contains(queryText)); } @@ -217,14 +237,18 @@ public async Task QueryMessageHistory( // Get context messages var messagesBefore = await _dbContext.Messages .Include(m => m.MessageExtensions) - .Where(m => m.GroupId == chatId && m.DateTime < msg.DateTime) + .Where(m => m.GroupId == chatId && + m.DateTime < msg.DateTime && + !invisibleUserIds.Contains(m.FromUserId)) .OrderByDescending(m => m.DateTime) .Take(5) .ToListAsync(); var messagesAfter = await _dbContext.Messages .Include(m => m.MessageExtensions) - .Where(m => m.GroupId == chatId && m.DateTime > msg.DateTime) + .Where(m => m.GroupId == chatId && + m.DateTime > msg.DateTime && + !invisibleUserIds.Contains(m.FromUserId)) .OrderBy(m => m.DateTime) .Take(5) .ToListAsync(); @@ -297,5 +321,37 @@ public async Task QueryMessageHistory( }; } } + + private async Task> GetInvisibleUserIdsAsync(long chatId) { + return _llmVisibilityService == null + ? new HashSet() + : await _llmVisibilityService.GetInvisibleUserIdsAsync(chatId); + } + + private (int totalHits, List messageDtos) SearchVisibleLuceneMessages( + string query, + long chatId, + int skip, + int take, + HashSet invisibleUserIds) { + var requiredVisibleCount = skip + take; + var fetchSize = Math.Max(requiredVisibleCount, 100); + const int maxFetchSize = 1000; + + while (true) { + var searchResult = _luceneManager.Search(query, chatId, 0, fetchSize); + var visibleDtos = searchResult.Item2 + .Where(m => !invisibleUserIds.Contains(m.FromUserId)) + .ToList(); + + if (visibleDtos.Count >= requiredVisibleCount || + searchResult.Item2.Count >= searchResult.Item1 || + fetchSize >= maxFetchSize) { + return (visibleDtos.Count, visibleDtos.Skip(skip).Take(take).ToList()); + } + + fetchSize = Math.Min(maxFetchSize, fetchSize * 2); + } + } } } From f7d4a648f9293f34dadf392c5cfbbe4476d3dbe3 Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Mon, 1 Jun 2026 19:38:16 +0800 Subject: [PATCH 2/4] Address LLM invisibility review feedback --- ..._AddLlmInvisibilityLookupIndex.Designer.cs | 898 ++++++++++++++++++ ...601113631_AddLlmInvisibilityLookupIndex.cs | 27 + .../Migrations/DataDbContextModelSnapshot.cs | 2 + .../Model/DataDbContext.cs | 3 + .../Service/AI/LLM/GeminiService.cs | 12 + .../Service/AI/LLM/LlmVisibilityService.cs | 3 +- .../Controller/AI/LLM/GeneralLLMController.cs | 10 +- .../Manage/LlmVisibilityController.cs | 21 +- .../Service/Manage/RefreshService.cs | 15 +- .../Service/Tools/SearchToolService.cs | 14 +- 10 files changed, 990 insertions(+), 15 deletions(-) create mode 100644 TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.Designer.cs create mode 100644 TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.cs diff --git a/TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.Designer.cs b/TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.Designer.cs new file mode 100644 index 00000000..3978cc1b --- /dev/null +++ b/TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.Designer.cs @@ -0,0 +1,898 @@ +// +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("20260601113631_AddLlmInvisibilityLookupIndex")] + partial class AddLlmInvisibilityLookupIndex + { + /// + 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("IsLlmInvisible") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId", "IsLlmInvisible"); + + 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/20260601113631_AddLlmInvisibilityLookupIndex.cs b/TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.cs new file mode 100644 index 00000000..39892026 --- /dev/null +++ b/TelegramSearchBot.Database/Migrations/20260601113631_AddLlmInvisibilityLookupIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TelegramSearchBot.Migrations +{ + /// + public partial class AddLlmInvisibilityLookupIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_UsersWithGroup_GroupId_IsLlmInvisible", + table: "UsersWithGroup", + columns: new[] { "GroupId", "IsLlmInvisible" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UsersWithGroup_GroupId_IsLlmInvisible", + table: "UsersWithGroup"); + } + } +} diff --git a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs index fab6b8bc..366cec3b 100644 --- a/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs +++ b/TelegramSearchBot.Database/Migrations/DataDbContextModelSnapshot.cs @@ -738,6 +738,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("GroupId", "IsLlmInvisible"); + b.HasIndex("UserId", "GroupId") .IsUnique(); diff --git a/TelegramSearchBot.Database/Model/DataDbContext.cs b/TelegramSearchBot.Database/Model/DataDbContext.cs index a5702e79..40703c84 100644 --- a/TelegramSearchBot.Database/Model/DataDbContext.cs +++ b/TelegramSearchBot.Database/Model/DataDbContext.cs @@ -36,6 +36,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { .HasIndex(uwg => new { uwg.UserId, uwg.GroupId }) .IsUnique(); + modelBuilder.Entity() + .HasIndex(uwg => new { uwg.GroupId, uwg.IsLlmInvisible }); + // 配置对话段模型 modelBuilder.Entity() .HasIndex(cs => new { cs.GroupId, cs.StartTime, cs.EndTime }); diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs index eebd5de7..a451f9c3 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs @@ -398,7 +398,19 @@ public async IAsyncEnumerable ExecAsync( var model = googleAI.CreateGenerativeModel("models/" + modelName); var fullResponse = new StringBuilder(); + if (_llmVisibilityService != null && + message != null && + await _llmVisibilityService.IsUserInvisibleAsync(ChatId, message.FromUserId, cancellationToken)) { + _chatSessions.Remove(ChatId); + yield break; + } + var history = await GetChatHistory(ChatId, message, await CheckVisionSupport(modelName, channel.Id)); + if (_llmVisibilityService != null && + ( await _llmVisibilityService.GetInvisibleUserIdsAsync(ChatId, cancellationToken) ).Count > 0) { + _chatSessions.Remove(ChatId); + } + if (!_chatSessions.TryGetValue(ChatId, out var chatSession)) { chatSession = model.StartChat(history: history); _chatSessions[ChatId] = chatSession; diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs index cac19536..d8613d0a 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/LlmVisibilityService.cs @@ -10,7 +10,7 @@ using TelegramSearchBot.Model.Data; namespace TelegramSearchBot.Service.AI.LLM { - [Injectable(ServiceLifetime.Transient)] + [Injectable(ServiceLifetime.Scoped)] public class LlmVisibilityService : IService { private readonly DataDbContext _dbContext; @@ -73,7 +73,6 @@ public async Task SetUserInvisibleAsync( await _dbContext.UsersWithGroup.AddAsync(userWithGroup, cancellationToken); } else { userWithGroup.IsLlmInvisible = isInvisible; - _dbContext.UsersWithGroup.Update(userWithGroup); } await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs b/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs index 95764e61..a1aefc15 100644 --- a/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs +++ b/TelegramSearchBot/Controller/AI/LLM/GeneralLLMController.cs @@ -248,7 +248,7 @@ public async Task ExecuteAsync(PipelineContext p) { if (fromUserId != 0 && await _llmVisibilityService.IsUserInvisibleAsync(telegramMessage.Chat.Id, fromUserId)) { logger.LogInformation("User {UserId} in chat {ChatId} is LLM invisible; skipping LLM execution for message {MessageId}.", fromUserId, telegramMessage.Chat.Id, telegramMessage.MessageId); - await SendMessageService.SendMessage("你已开启 LLM 隐身,这条消息不会发送给 LLM。发送 `取消LLM隐身` 可恢复。", telegramMessage.Chat.Id, telegramMessage.MessageId); + await SendLlmInvisibleNoticeAsync(telegramMessage.Chat.Id, fromUserId, telegramMessage.MessageId); return; } @@ -539,6 +539,14 @@ private static string GetMusicGenerationModelSelectionKey(long chatId, long user return LlmAgentRedisKeys.MusicGenerationModelSelection(chatId, userId); } + private async Task SendLlmInvisibleNoticeAsync(long chatId, long userId, int replyToMessageId) { + var db = _connectionMultiplexer.GetDatabase(); + var key = $"llm_invisible_notice:{chatId}:{userId}"; + if (await db.StringSetAsync(key, "1", TimeSpan.FromMinutes(10), When.NotExists)) { + await SendMessageService.SendMessage("你已开启 LLM 隐身,这条消息不会发送给 LLM。发送 `取消LLM隐身` 可恢复。", chatId, replyToMessageId); + } + } + private sealed record ImageGenerationModelSelectionOption(string ModelName, string ChannelSummary); private sealed record MusicGenerationModelSelectionOption(string ModelName, string ChannelSummary); } diff --git a/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs index 78b1e7e6..bf642910 100644 --- a/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs +++ b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs @@ -28,7 +28,7 @@ public async Task ExecuteAsync(PipelineContext p) { return; } - var command = ( message.Text ?? message.Caption ?? string.Empty ).Trim(); + var command = NormalizeCommand(message.Text ?? message.Caption ?? string.Empty); if (string.IsNullOrWhiteSpace(command)) { return; } @@ -52,6 +52,25 @@ public async Task ExecuteAsync(PipelineContext p) { } } + private static string NormalizeCommand(string rawCommand) { + var command = rawCommand.Trim(); + if (string.IsNullOrWhiteSpace(command)) { + return string.Empty; + } + + var tokenEnd = command.IndexOfAny(new[] { ' ', '\r', '\n', '\t' }); + if (tokenEnd >= 0) { + command = command.Substring(0, tokenEnd); + } + + var botSuffixStart = command.IndexOf('@'); + if (botSuffixStart >= 0) { + command = command.Substring(0, botSuffixStart); + } + + return command.Trim(); + } + private static bool IsEnableCommand(string command) { return command.Equals("LLM隐身", StringComparison.OrdinalIgnoreCase) || command.Equals("/llm_invisible_on", StringComparison.OrdinalIgnoreCase); diff --git a/TelegramSearchBot/Service/Manage/RefreshService.cs b/TelegramSearchBot/Service/Manage/RefreshService.cs index 52dd6854..61e56748 100644 --- a/TelegramSearchBot/Service/Manage/RefreshService.cs +++ b/TelegramSearchBot/Service/Manage/RefreshService.cs @@ -315,6 +315,14 @@ private async Task ScanAndProcessAltImageFiles() { await Send.Log($"开始处理图片Alt信息,共{totalFiles}个文件"); + async Task ReportProgressAsync() { + processedFiles++; + if (filesPerPercent > 0 && processedFiles >= nextPercent * filesPerPercent) { + await Send.Log($"图片Alt处理进度: {nextPercent}% ({processedFiles}/{totalFiles})"); + nextPercent++; + } + } + foreach (var chatDir in chatDirs) { var chatId = long.Parse(Path.GetFileName(chatDir)); var imageFiles = Directory.GetFiles(chatDir); @@ -329,6 +337,7 @@ private async Task ScanAndProcessAltImageFiles() { .FirstOrDefaultAsync(m => m.Id == messageDataId.Value); if (message != null && await _llmVisibilityService.IsUserInvisibleAsync(chatId, message.FromUserId)) { + await ReportProgressAsync(); continue; } @@ -344,11 +353,7 @@ await _llmVisibilityService.IsUserInvisibleAsync(chatId, message.FromUserId)) { } } - processedFiles++; - if (filesPerPercent > 0 && processedFiles >= nextPercent * filesPerPercent) { - await Send.Log($"图片Alt处理进度: {nextPercent}% ({processedFiles}/{totalFiles})"); - nextPercent++; - } + await ReportProgressAsync(); } } await Send.Log($"图片Alt处理完成: 100% ({totalFiles}/{totalFiles})"); diff --git a/TelegramSearchBot/Service/Tools/SearchToolService.cs b/TelegramSearchBot/Service/Tools/SearchToolService.cs index 2ab88428..e654ca99 100644 --- a/TelegramSearchBot/Service/Tools/SearchToolService.cs +++ b/TelegramSearchBot/Service/Tools/SearchToolService.cs @@ -30,11 +30,11 @@ public SearchToolService( LuceneManager luceneManager, DataDbContext dbContext, MessageExtensionService messageExtensionService, - LlmVisibilityService llmVisibilityService = null) { + LlmVisibilityService llmVisibilityService) { _luceneManager = luceneManager; _dbContext = dbContext; _messageExtensionService = messageExtensionService; - _llmVisibilityService = llmVisibilityService; + _llmVisibilityService = llmVisibilityService ?? throw new ArgumentNullException(nameof(llmVisibilityService)); } [BuiltInTool("Searches indexed messages within the current chat using keywords. Supports pagination.")] @@ -323,9 +323,7 @@ public async Task QueryMessageHistory( } private async Task> GetInvisibleUserIdsAsync(long chatId) { - return _llmVisibilityService == null - ? new HashSet() - : await _llmVisibilityService.GetInvisibleUserIdsAsync(chatId); + return await _llmVisibilityService.GetInvisibleUserIdsAsync(chatId); } private (int totalHits, List messageDtos) SearchVisibleLuceneMessages( @@ -347,7 +345,11 @@ private async Task> GetInvisibleUserIdsAsync(long chatId) { if (visibleDtos.Count >= requiredVisibleCount || searchResult.Item2.Count >= searchResult.Item1 || fetchSize >= maxFetchSize) { - return (visibleDtos.Count, visibleDtos.Skip(skip).Take(take).ToList()); + var invisibleHitsInFetchedSet = searchResult.Item2.Count(m => invisibleUserIds.Contains(m.FromUserId)); + var totalVisibleFound = searchResult.Item2.Count >= searchResult.Item1 + ? visibleDtos.Count + : Math.Max(0, searchResult.Item1 - invisibleHitsInFetchedSet); + return (totalVisibleFound, visibleDtos.Skip(skip).Take(take).ToList()); } fetchSize = Math.Min(maxFetchSize, fetchSize * 2); From ed562fb075163f208c8b7fcf3406686ba7a4a29b Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Mon, 1 Jun 2026 19:46:44 +0800 Subject: [PATCH 3/4] Register LLM invisibility bot commands --- .../Service/BotAPI/TelegramCommandRegistryService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TelegramSearchBot/Service/BotAPI/TelegramCommandRegistryService.cs b/TelegramSearchBot/Service/BotAPI/TelegramCommandRegistryService.cs index 2d8cb301..66661f1c 100644 --- a/TelegramSearchBot/Service/BotAPI/TelegramCommandRegistryService.cs +++ b/TelegramSearchBot/Service/BotAPI/TelegramCommandRegistryService.cs @@ -26,6 +26,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { new BotCommand { Command = "resolveurls", Description = "解析文本中的链接并存储原始链接与解析后链接的映射。" }, new BotCommand { Command = "checkupdate", Description = "检查系统更新状态。" }, new BotCommand { Command = "update", Description = "执行系统更新(如果存在新版本)。" }, + new BotCommand { Command = "llm_invisible_on", Description = "开启当前群的 LLM 隐身。" }, + new BotCommand { Command = "llm_invisible_off", Description = "关闭当前群的 LLM 隐身。" }, + new BotCommand { Command = "llm_invisible_status", Description = "查看当前群的 LLM 隐身状态。" }, }; _logger.LogInformation($"Registering {commands.Count} commands..."); From 3e3eb77d3572a256d7fab5b14b55b6ee262eb4ea Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Mon, 1 Jun 2026 20:11:49 +0800 Subject: [PATCH 4/4] Handle bot suffixes in LLM invisibility commands --- .../Manage/LlmVisibilityControllerTests.cs | 69 +++++++++++++++++++ .../Manage/LlmVisibilityController.cs | 19 ++--- 2 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 TelegramSearchBot.Test/Controller/Manage/LlmVisibilityControllerTests.cs diff --git a/TelegramSearchBot.Test/Controller/Manage/LlmVisibilityControllerTests.cs b/TelegramSearchBot.Test/Controller/Manage/LlmVisibilityControllerTests.cs new file mode 100644 index 00000000..309c3281 --- /dev/null +++ b/TelegramSearchBot.Test/Controller/Manage/LlmVisibilityControllerTests.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using TelegramSearchBot.Controller.Manage; +using Xunit; + +namespace TelegramSearchBot.Test.Controller.Manage; + +public class LlmVisibilityControllerTests +{ + [Theory] + [InlineData("/llm_invisible_on")] + [InlineData("/llm_invisible_on@TelegramSearchBot")] + [InlineData("/LLM_INVISIBLE_ON@TelegramSearchBot")] + [InlineData("LLM隐身")] + public void IsEnableCommand_MatchesSupportedCommands(string command) + { + Assert.True(InvokeBool("IsEnableCommand", command)); + } + + [Theory] + [InlineData("/llm_invisible_off")] + [InlineData("/llm_invisible_off@TelegramSearchBot")] + [InlineData("取消LLM隐身")] + [InlineData("LLM显身")] + public void IsDisableCommand_MatchesSupportedCommands(string command) + { + Assert.True(InvokeBool("IsDisableCommand", command)); + } + + [Theory] + [InlineData("/llm_invisible_status")] + [InlineData("/llm_invisible_status@TelegramSearchBot")] + [InlineData("LLM隐身状态")] + public void IsStatusCommand_MatchesSupportedCommands(string command) + { + Assert.True(InvokeBool("IsStatusCommand", command)); + } + + [Theory] + [InlineData("/llm_invisible_on_bad")] + [InlineData("/llm_invisible_off_bad")] + [InlineData("/llm_invisible_status_bad")] + public void SlashCommandMatching_DoesNotMatchLongerNames(string command) + { + Assert.False(InvokeBool("IsEnableCommand", command)); + Assert.False(InvokeBool("IsDisableCommand", command)); + Assert.False(InvokeBool("IsStatusCommand", command)); + } + + [Fact] + public void NormalizeCommand_PreservesBotSuffixAndDropsArguments() + { + var method = typeof(LlmVisibilityController).GetMethod( + "NormalizeCommand", + BindingFlags.NonPublic | BindingFlags.Static)!; + + var result = (string)method.Invoke(null, new object[] { "/llm_invisible_on@TelegramSearchBot extra" })!; + + Assert.Equal("/llm_invisible_on@TelegramSearchBot", result); + } + + private static bool InvokeBool(string methodName, string command) + { + var method = typeof(LlmVisibilityController).GetMethod( + methodName, + BindingFlags.NonPublic | BindingFlags.Static)!; + + return (bool)method.Invoke(null, new object[] { command })!; + } +} diff --git a/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs index bf642910..49606337 100644 --- a/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs +++ b/TelegramSearchBot/Controller/Manage/LlmVisibilityController.cs @@ -63,28 +63,31 @@ private static string NormalizeCommand(string rawCommand) { command = command.Substring(0, tokenEnd); } - var botSuffixStart = command.IndexOf('@'); - if (botSuffixStart >= 0) { - command = command.Substring(0, botSuffixStart); - } - return command.Trim(); } private static bool IsEnableCommand(string command) { return command.Equals("LLM隐身", StringComparison.OrdinalIgnoreCase) || - command.Equals("/llm_invisible_on", StringComparison.OrdinalIgnoreCase); + IsSlashCommand(command, "/llm_invisible_on"); } private static bool IsDisableCommand(string command) { return command.Equals("取消LLM隐身", StringComparison.OrdinalIgnoreCase) || command.Equals("LLM显身", StringComparison.OrdinalIgnoreCase) || - command.Equals("/llm_invisible_off", StringComparison.OrdinalIgnoreCase); + IsSlashCommand(command, "/llm_invisible_off"); } private static bool IsStatusCommand(string command) { return command.Equals("LLM隐身状态", StringComparison.OrdinalIgnoreCase) || - command.Equals("/llm_invisible_status", StringComparison.OrdinalIgnoreCase); + IsSlashCommand(command, "/llm_invisible_status"); + } + + private static bool IsSlashCommand(string command, string expectedCommand) { + if (!command.StartsWith(expectedCommand, StringComparison.OrdinalIgnoreCase)) { + return false; + } + + return command.Length == expectedCommand.Length || command[expectedCommand.Length] == '@'; } } }