From 9c45b750a1c747ce5bd36ce8f4d47ce5770c1906 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:33:22 +0000 Subject: [PATCH 1/5] Initial plan From fd02e1664feac0ab398610c00f43b1fe97595188 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:47:48 +0000 Subject: [PATCH 2/5] feat: embed telegram-bot-api as a managed child process with ClickOnce bundle support - Add EnableLocalBotAPI, TelegramBotApiId, TelegramBotApiHash, LocalBotApiPort to Config/Env - Auto-set BaseUrl and IsLocalAPI when EnableLocalBotAPI is true - Launch telegram-bot-api.exe as a ChildProcessManager-managed process in GeneralBootstrap - Add conditional Content entry for telegram-bot-api.exe in .csproj - Build telegram-bot-api from source via cmake+vcpkg in GitHub Actions push workflow Co-authored-by: ModerRAS <28183976+ModerRAS@users.noreply.github.com> --- .github/workflows/push.yml | 30 +++++++++++ TelegramSearchBot.Common/Env.cs | 21 +++++++- .../AppBootstrap/GeneralBootstrap.cs | 54 +++++++++++++++++++ TelegramSearchBot/TelegramSearchBot.csproj | 5 ++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 918c69dd..cf750ad3 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -25,6 +25,36 @@ jobs: uses: microsoft/setup-msbuild@v1.1 - name: Clear NuGet cache run: dotnet nuget locals all --clear + - name: Cache telegram-bot-api build + id: cache-tg-bot-api + uses: actions/cache@v4 + with: + path: telegram-bot-api-bin + key: tg-bot-api-win-x64-${{ hashFiles('.github/workflows/push.yml') }} + restore-keys: | + tg-bot-api-win-x64- + - name: Build telegram-bot-api from source + if: steps.cache-tg-bot-api.outputs.cache-hit != 'true' + shell: pwsh + run: | + git clone --recursive https://github.com/tdlib/telegram-bot-api.git + C:\vcpkg\vcpkg.exe install openssl:x64-windows-static zlib:x64-windows-static --no-print-usage + Push-Location telegram-bot-api + New-Item -ItemType Directory -Force build | Out-Null + Push-Location build + cmake -A x64 -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_TOOLCHAIN_FILE="C:/vcpkg/scripts/buildsystems/vcpkg.cmake" ` + -DVCPKG_TARGET_TRIPLET="x64-windows-static" ` + .. + cmake --build . --config Release --target telegram-bot-api + Pop-Location + Pop-Location + New-Item -ItemType Directory -Force telegram-bot-api-bin | Out-Null + Copy-Item "telegram-bot-api\build\Release\telegram-bot-api.exe" "telegram-bot-api-bin\telegram-bot-api.exe" + - name: Copy telegram-bot-api binary to project + shell: pwsh + run: | + Copy-Item "telegram-bot-api-bin\telegram-bot-api.exe" "TelegramSearchBot\telegram-bot-api.exe" - name: Restore dependencies run: dotnet restore --force --no-cache /p:BuildWithNetFrameworkHostedCompiler=true - name: Build diff --git a/TelegramSearchBot.Common/Env.cs b/TelegramSearchBot.Common/Env.cs index eaf50513..854a57a3 100644 --- a/TelegramSearchBot.Common/Env.cs +++ b/TelegramSearchBot.Common/Env.cs @@ -13,8 +13,17 @@ static Env() { } try { var config = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(WorkDir, "Config.json"))); - BaseUrl = config.BaseUrl; - IsLocalAPI = config.IsLocalAPI; + EnableLocalBotAPI = config.EnableLocalBotAPI; + TelegramBotApiId = config.TelegramBotApiId; + TelegramBotApiHash = config.TelegramBotApiHash; + LocalBotApiPort = config.LocalBotApiPort; + if (config.EnableLocalBotAPI) { + BaseUrl = $"http://127.0.0.1:{config.LocalBotApiPort}"; + IsLocalAPI = true; + } else { + BaseUrl = config.BaseUrl; + IsLocalAPI = config.IsLocalAPI; + } BotToken = config.BotToken; AdminId = config.AdminId; EnableAutoOCR = config.EnableAutoOCR; @@ -44,6 +53,10 @@ static Env() { public static readonly long AdminId; public static readonly bool EnableAutoOCR; public static readonly bool EnableAutoASR; + public static readonly bool EnableLocalBotAPI; + public static readonly string TelegramBotApiId; + public static readonly string TelegramBotApiHash; + public static readonly int LocalBotApiPort; public static readonly string WorkDir; public static readonly int TaskDelayTimeout; public static readonly bool SameServer; @@ -70,6 +83,10 @@ public class Config { public bool EnableAutoASR { get; set; } = false; //public string WorkDir { get; set; } = "/data/TelegramSearchBot"; public bool IsLocalAPI { get; set; } = false; + public bool EnableLocalBotAPI { get; set; } = false; + public string TelegramBotApiId { get; set; } + public string TelegramBotApiHash { get; set; } + public int LocalBotApiPort { get; set; } = 8081; public bool SameServer { get; set; } = false; public int TaskDelayTimeout { get; set; } = 1000; public string OllamaModelName { get; set; } = "qwen2.5:72b-instruct-q2_K"; diff --git a/TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs b/TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs index 14503d52..2a000128 100644 --- a/TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs +++ b/TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -52,6 +53,23 @@ private static async Task WaitForGarnetReady(int port, int maxRetries = 20, int Log.Warning("等待 Garnet 服务就绪超时 (端口 {Port}),将继续启动(Redis 连接会自动重试)", port); } + /// + /// 等待本地 telegram-bot-api 服务端口就绪,最多等待 20 秒 + /// + private static async Task WaitForLocalBotApiReady(int port, int maxRetries = 40, int delayMs = 500) { + for (int i = 0; i < maxRetries; i++) { + try { + using var tcp = new System.Net.Sockets.TcpClient(); + await tcp.ConnectAsync("127.0.0.1", port); + Log.Information("telegram-bot-api 服务已就绪 (端口 {Port}),耗时约 {ElapsedMs}ms", port, i * delayMs); + return; + } catch { + await Task.Delay(delayMs); + } + } + Log.Warning("等待 telegram-bot-api 服务就绪超时 (端口 {Port}),将继续启动", port); + } + public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() @@ -73,6 +91,42 @@ public static async Task Startup(string[] args) { // 等待 Garnet 服务就绪,避免竞态条件导致 Redis 连接失败 await WaitForGarnetReady(Env.SchedulerPort); + // 如果启用了本地 telegram-bot-api,则在此启动它 + if (Env.EnableLocalBotAPI) { + string botApiExePath = Path.Combine(AppContext.BaseDirectory, "telegram-bot-api.exe"); + if (File.Exists(botApiExePath)) { + if (string.IsNullOrEmpty(Env.TelegramBotApiId) || string.IsNullOrEmpty(Env.TelegramBotApiHash)) { + Log.Warning("EnableLocalBotAPI 为 true,但 TelegramBotApiId 或 TelegramBotApiHash 未配置,跳过本地 Bot API 启动"); + } else { + var botApiDataDir = Path.Combine(Env.WorkDir, "telegram-bot-api"); + Directory.CreateDirectory(botApiDataDir); + // 使用 ArgumentList 以正确处理路径中的空格 + // --local 模式允许大文件上传下载并将文件存储在本地 dir 下 + var startInfo = new ProcessStartInfo { + FileName = botApiExePath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.ArgumentList.Add("--local"); + startInfo.ArgumentList.Add($"--api-id={Env.TelegramBotApiId}"); + startInfo.ArgumentList.Add($"--api-hash={Env.TelegramBotApiHash}"); + startInfo.ArgumentList.Add($"--dir={botApiDataDir}"); + startInfo.ArgumentList.Add($"--http-port={Env.LocalBotApiPort}"); + var botApiProcess = Process.Start(startInfo); + if (botApiProcess == null) { + Log.Warning("telegram-bot-api 进程启动失败"); + } else { + childProcessManager.AddProcess(botApiProcess); + Log.Information("telegram-bot-api 已启动,等待端口 {Port} 就绪...", Env.LocalBotApiPort); + await WaitForLocalBotApiReady(Env.LocalBotApiPort); + } + } + } else { + Log.Warning("未找到 telegram-bot-api 可执行文件 {Path},跳过本地 Bot API 启动", botApiExePath); + } + } + IHost host = CreateHostBuilder(args) //.ConfigureLogging(logging => { // logging.ClearProviders(); diff --git a/TelegramSearchBot/TelegramSearchBot.csproj b/TelegramSearchBot/TelegramSearchBot.csproj index df2a4a73..7355a97a 100644 --- a/TelegramSearchBot/TelegramSearchBot.csproj +++ b/TelegramSearchBot/TelegramSearchBot.csproj @@ -96,6 +96,11 @@ + + + Always + + From b8431f8132ca9c34e0277595d90c062e46a2ba36 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 19 Mar 2026 11:45:52 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(OCR):=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E5=A4=A7=E6=A8=A1=E5=9E=8B=E7=9A=84OCR=E4=BD=9C?= =?UTF-8?q?=E4=B8=BAPaddleOCR=E7=9A=84=E6=9B=BF=E4=BB=A3=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增IOCRService接口和OCREngine枚举 - 新增LLMOCRService实现,使用GeneralLLMService.AnalyzeImageAsync - 新增OCRConfState枚举和状态机 - 新增EditOCRConfRedisHelper用于Redis状态存储 - 新增EditOCRConfService实现OCR配置状态机逻辑 - 新增EditOCRConfController和EditOCRConfView用于Bot私聊配置 - 修改AutoOCRController支持OCR引擎动态切换 - 修改PaddleOCRService实现IOCRService接口 支持通过Bot私聊发送"OCR设置"进行配置 --- .../Controller/AI/OCR/AutoOCRController.cs | 30 ++- .../Manage/EditOCRConfController.cs | 59 +++++ .../Helper/EditOCRConfRedisHelper.cs | 61 +++++ .../Interface/AI/OCR/IOCRService.cs | 14 + TelegramSearchBot/Model/AI/OCRConfState.cs | 28 ++ .../Service/AI/OCR/LLMOCRService.cs | 58 +++++ .../Service/AI/OCR/PaddleOCRService.cs | 3 +- .../Service/Manage/EditOCRConfService.cs | 240 ++++++++++++++++++ TelegramSearchBot/View/EditOCRConfView.cs | 40 +++ 9 files changed, 527 insertions(+), 6 deletions(-) create mode 100644 TelegramSearchBot/Controller/Manage/EditOCRConfController.cs create mode 100644 TelegramSearchBot/Helper/EditOCRConfRedisHelper.cs create mode 100644 TelegramSearchBot/Interface/AI/OCR/IOCRService.cs create mode 100644 TelegramSearchBot/Model/AI/OCRConfState.cs create mode 100644 TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs create mode 100644 TelegramSearchBot/Service/Manage/EditOCRConfService.cs create mode 100644 TelegramSearchBot/View/EditOCRConfView.cs diff --git a/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs b/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs index 0ca6ade8..dd77b63c 100644 --- a/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs +++ b/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Telegram.Bot; @@ -18,13 +19,17 @@ using TelegramSearchBot.Interface.Controller; using TelegramSearchBot.Manager; using TelegramSearchBot.Model; +using TelegramSearchBot.Model.AI; using TelegramSearchBot.Service.AI.OCR; using TelegramSearchBot.Service.BotAPI; +using TelegramSearchBot.Service.Common; +using TelegramSearchBot.Service.Manage; using TelegramSearchBot.Service.Storage; namespace TelegramSearchBot.Controller.AI.OCR { public class AutoOCRController : IOnUpdate { - private readonly IPaddleOCRService paddleOCRService; + private readonly IEnumerable _ocrServices; + private readonly IAppConfigurationService _configService; private readonly MessageService messageService; private readonly ITelegramBotClient botClient; private readonly SendMessage Send; @@ -33,14 +38,16 @@ public class AutoOCRController : IOnUpdate { private readonly MessageExtensionService MessageExtensionService; public AutoOCRController( ITelegramBotClient botClient, - IPaddleOCRService paddleOCRService, + IEnumerable ocrServices, + IAppConfigurationService configService, SendMessage Send, MessageService messageService, ILogger logger, ISendMessageService sendMessageService, MessageExtensionService messageExtensionService ) { - this.paddleOCRService = paddleOCRService; + this._ocrServices = ocrServices; + this._configService = configService; this.messageService = messageService; this.botClient = botClient; this.Send = Send; @@ -63,7 +70,13 @@ public async Task ExecuteAsync(PipelineContext p) { try { var PhotoStream = await IProcessPhoto.GetPhoto(e); logger.LogInformation($"Get Photo File: {e.Message.Chat.Id}/{e.Message.MessageId}"); - OcrStr = await paddleOCRService.ExecuteAsync(new MemoryStream(PhotoStream)); + + var engine = await GetOCREngineAsync(); + var ocrService = _ocrServices.FirstOrDefault(s => s.Engine == engine) + ?? _ocrServices.First(s => s.Engine == OCREngine.PaddleOCR); + + logger.LogInformation($"使用OCR引擎: {engine}"); + OcrStr = await ocrService.ExecuteAsync(new MemoryStream(PhotoStream)); if (!string.IsNullOrWhiteSpace(OcrStr)) { logger.LogInformation(OcrStr); await MessageExtensionService.AddOrUpdateAsync(p.MessageDataId, "OCR_Result", OcrStr); @@ -80,7 +93,6 @@ ex is DirectoryNotFoundException ( e.Message.ReplyToMessage != null && e.Message.Text != null && e.Message.Text.Equals("打印") )) { string ocrResult = OcrStr; - // 如果是回复消息触发打印 if (e.Message.ReplyToMessage != null) { var originalMessageId = await MessageExtensionService.GetMessageIdByMessageIdAndGroupId( e.Message.ReplyToMessage.MessageId, @@ -100,5 +112,13 @@ ex is DirectoryNotFoundException } } } + + private async Task GetOCREngineAsync() { + var engineStr = await _configService.GetConfigurationValueAsync(EditOCRConfService.OCREngineKey); + if (!string.IsNullOrEmpty(engineStr) && Enum.TryParse(engineStr, out var engine)) { + return engine; + } + return OCREngine.PaddleOCR; + } } } diff --git a/TelegramSearchBot/Controller/Manage/EditOCRConfController.cs b/TelegramSearchBot/Controller/Manage/EditOCRConfController.cs new file mode 100644 index 00000000..9a563221 --- /dev/null +++ b/TelegramSearchBot/Controller/Manage/EditOCRConfController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Types; +using TelegramSearchBot.Common; +using TelegramSearchBot.Interface.Controller; +using TelegramSearchBot.Manager; +using TelegramSearchBot.Model; +using TelegramSearchBot.Service.BotAPI; +using TelegramSearchBot.Service.Manage; +using TelegramSearchBot.View; + +namespace TelegramSearchBot.Controller.Manage { + public class EditOCRConfController : IOnUpdate { + protected readonly AdminService AdminService; + protected readonly EditOCRConfService EditOCRConfService; + protected readonly EditOCRConfView EditOCRConfView; + public ITelegramBotClient botClient { get; set; } + public EditOCRConfController( + ITelegramBotClient botClient, + AdminService AdminService, + EditOCRConfService EditOCRConfService, + EditOCRConfView EditOCRConfView) { + this.AdminService = AdminService; + this.EditOCRConfService = EditOCRConfService; + this.EditOCRConfView = EditOCRConfView; + this.botClient = botClient; + } + public List Dependencies => new List(); + + public async Task ExecuteAsync(PipelineContext p) { + var e = p.Update; + if (e?.Message?.Chat?.Id < 0) { + return; + } + if (e?.Message?.From?.Id != Env.AdminId) { + return; + } + string Command; + if (!string.IsNullOrEmpty(e.Message.Text)) { + Command = e.Message.Text; + } else if (!string.IsNullOrEmpty(e.Message.Caption)) { + Command = e.Message.Caption; + } else return; + + var (status, message) = await EditOCRConfService.ExecuteAsync(Command, e.Message.Chat.Id); + if (status) { + await EditOCRConfView + .WithChatId(e.Message.Chat.Id) + .WithReplyTo(e.Message.MessageId) + .WithMessage(message) + .Render(); + } + } + } +} diff --git a/TelegramSearchBot/Helper/EditOCRConfRedisHelper.cs b/TelegramSearchBot/Helper/EditOCRConfRedisHelper.cs new file mode 100644 index 00000000..59714f86 --- /dev/null +++ b/TelegramSearchBot/Helper/EditOCRConfRedisHelper.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace TelegramSearchBot.Helper { + public class EditOCRConfRedisHelper { + private readonly IConnectionMultiplexer _connection; + private readonly long _chatId; + private IDatabase _database; + + public EditOCRConfRedisHelper(IConnectionMultiplexer connection, long chatId) { + _connection = connection; + _chatId = chatId; + _database = _connection.GetDatabase(); + } + + private IDatabase GetDatabase() => _database; + + private string StateKey => $"ocrconf:{_chatId}:state"; + private string DataKey => $"ocrconf:{_chatId}:data"; + private string ChannelKey => $"ocrconf:{_chatId}:channel"; + + public async Task GetStateAsync() { + return await GetDatabase().StringGetAsync(StateKey); + } + + public async Task SetStateAsync(string state) { + await GetDatabase().StringSetAsync(StateKey, state); + } + + public async Task GetDataAsync() { + return await GetDatabase().StringGetAsync(DataKey); + } + + public async Task SetDataAsync(string data) { + await GetDatabase().StringSetAsync(DataKey, data); + } + + public async Task GetChannelIdAsync() { + var value = await GetDatabase().StringGetAsync(ChannelKey); + if (value.HasValue && int.TryParse(value.ToString(), out var channelId)) { + return channelId; + } + return null; + } + + public async Task SetChannelIdAsync(int channelId) { + await GetDatabase().StringSetAsync(ChannelKey, channelId.ToString()); + } + + public async Task DeleteKeysAsync() { + var db = GetDatabase(); + await db.KeyDeleteAsync(StateKey); + await db.KeyDeleteAsync(DataKey); + await db.KeyDeleteAsync(ChannelKey); + } + } +} diff --git a/TelegramSearchBot/Interface/AI/OCR/IOCRService.cs b/TelegramSearchBot/Interface/AI/OCR/IOCRService.cs new file mode 100644 index 00000000..b82314cd --- /dev/null +++ b/TelegramSearchBot/Interface/AI/OCR/IOCRService.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading.Tasks; + +namespace TelegramSearchBot.Interface.AI.OCR { + public enum OCREngine { + PaddleOCR, + LLM + } + + public interface IOCRService { + OCREngine Engine { get; } + Task ExecuteAsync(Stream file); + } +} diff --git a/TelegramSearchBot/Model/AI/OCRConfState.cs b/TelegramSearchBot/Model/AI/OCRConfState.cs new file mode 100644 index 00000000..a97a289a --- /dev/null +++ b/TelegramSearchBot/Model/AI/OCRConfState.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; + +namespace TelegramSearchBot.Model.AI { + public enum OCRConfState { + [Description("main_menu")] + MainMenu, + + [Description("selecting_engine")] + SelectingEngine, + + [Description("selecting_llm_channel")] + SelectingLLMChannel, + + [Description("selecting_llm_model")] + SelectingLLMModel, + + [Description("viewing_config")] + ViewingConfig + } + + public static class OCRConfStateExtensions { + public static string GetDescription(this OCRConfState state) { + var fieldInfo = state.GetType().GetField(state.ToString()); + var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + return attributes.Length > 0 ? attributes[0].Description : state.ToString(); + } + } +} diff --git a/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs b/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs new file mode 100644 index 00000000..cd1b018d --- /dev/null +++ b/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SkiaSharp; +using TelegramSearchBot.Attributes; +using TelegramSearchBot.Interface.AI.OCR; +using TelegramSearchBot.Interface.AI.LLM; + +namespace TelegramSearchBot.Service.AI.OCR { + [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)] + public class LLMOCRService : IOCRService { + private readonly IGeneralLLMService _generalLLMService; + private readonly ILogger _logger; + + public OCREngine Engine => OCREngine.LLM; + + public LLMOCRService( + IGeneralLLMService generalLLMService, + ILogger logger + ) { + _generalLLMService = generalLLMService; + _logger = logger; + } + + public async Task ExecuteAsync(Stream file) { + try { + var tg_img = SKBitmap.Decode(file); + var tg_img_data = tg_img.Encode(SKEncodedImageFormat.Jpeg, 99); + var tempPath = Path.GetTempFileName() + ".jpg"; + + try { + using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { + tg_img_data.SaveTo(fs); + } + + _logger.LogInformation("正在使用LLM进行OCR识别..."); + var result = await _generalLLMService.AnalyzeImageAsync(tempPath, 0); + + if (string.IsNullOrWhiteSpace(result)) { + _logger.LogWarning("LLM OCR返回空结果"); + return string.Empty; + } + + _logger.LogInformation("LLM OCR识别完成"); + return result; + } finally { + if (File.Exists(tempPath)) { + File.Delete(tempPath); + } + } + } catch (Exception ex) { + _logger.LogError(ex, "LLM OCR处理失败"); + throw; + } + } + } +} diff --git a/TelegramSearchBot/Service/AI/OCR/PaddleOCRService.cs b/TelegramSearchBot/Service/AI/OCR/PaddleOCRService.cs index 4a7064c9..93e9b1b4 100644 --- a/TelegramSearchBot/Service/AI/OCR/PaddleOCRService.cs +++ b/TelegramSearchBot/Service/AI/OCR/PaddleOCRService.cs @@ -9,9 +9,10 @@ namespace TelegramSearchBot.Service.AI.OCR { [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)] - public class PaddleOCRService : SubProcessService, IPaddleOCRService { + public class PaddleOCRService : SubProcessService, IPaddleOCRService, IOCRService { public new string ServiceName => "PaddleOCRService"; + public OCREngine Engine => OCREngine.PaddleOCR; public PaddleOCRService(IConnectionMultiplexer connectionMultiplexer) : base(connectionMultiplexer) { ForkName = "OCR"; diff --git a/TelegramSearchBot/Service/Manage/EditOCRConfService.cs b/TelegramSearchBot/Service/Manage/EditOCRConfService.cs new file mode 100644 index 00000000..fdf5f565 --- /dev/null +++ b/TelegramSearchBot/Service/Manage/EditOCRConfService.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using TelegramSearchBot.Attributes; +using TelegramSearchBot.Helper; +using TelegramSearchBot.Interface; +using TelegramSearchBot.Interface.AI.OCR; +using TelegramSearchBot.Interface.Manage; +using TelegramSearchBot.Model; +using TelegramSearchBot.Model.AI; +using TelegramSearchBot.Model.Data; + +namespace TelegramSearchBot.Service.Manage { + [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient)] + public class EditOCRConfService : IService { + public string ServiceName => "EditOCRConfService"; + protected readonly DataDbContext DataContext; + protected readonly ILogger _logger; + protected IConnectionMultiplexer connectionMultiplexer { get; set; } + + public const string OCREngineKey = "OCR:Engine"; + public const string OCRLLMModelNameKey = "OCR:LLMModelName"; + public const string OCRLLMChannelIdKey = "OCR:LLMChannelId"; + + private readonly Dictionary>> _stateHandlers; + + public EditOCRConfService( + DataDbContext context, + IConnectionMultiplexer connectionMultiplexer, + ILogger logger + ) { + this.connectionMultiplexer = connectionMultiplexer; + DataContext = context; + _logger = logger; + + _stateHandlers = new Dictionary>> + { + { OCRConfState.MainMenu.GetDescription(), HandleMainMenuAsync }, + { OCRConfState.SelectingEngine.GetDescription(), HandleSelectingEngineAsync }, + { OCRConfState.SelectingLLMChannel.GetDescription(), HandleSelectingLLMChannelAsync }, + { OCRConfState.SelectingLLMModel.GetDescription(), HandleSelectingLLMModelAsync }, + { OCRConfState.ViewingConfig.GetDescription(), HandleViewingConfigAsync } + }; + } + + public async Task<(bool, string)> ExecuteAsync(string command, long chatId) { + var redis = new EditOCRConfRedisHelper(connectionMultiplexer, chatId); + + var currentState = await redis.GetStateAsync(); + if (string.IsNullOrEmpty(currentState)) { + currentState = OCRConfState.MainMenu.GetDescription(); + await redis.SetStateAsync(currentState); + } + + if (command == "退出" || command == "返回") { + await redis.DeleteKeysAsync(); + return (true, "已退出OCR配置"); + } + + if (command == "OCR设置" || command == "OCR配置") { + currentState = OCRConfState.MainMenu.GetDescription(); + await redis.SetStateAsync(currentState); + } + + if (_stateHandlers.TryGetValue(currentState, out var handler)) { + return await handler(redis, command); + } + + return (false, string.Empty); + } + + private async Task<(bool, string)> HandleMainMenuAsync(EditOCRConfRedisHelper redis, string command) { + var sb = new StringBuilder(); + sb.AppendLine("🔧 OCR配置"); + sb.AppendLine(); + + var currentEngine = await GetCurrentEngineAsync(); + sb.AppendLine($"当前OCR引擎: {currentEngine}"); + sb.AppendLine(); + sb.AppendLine("请选择操作:"); + sb.AppendLine("1. 切换OCR引擎"); + sb.AppendLine("2. 查看配置详情"); + sb.AppendLine("3. 返回"); + + return (true, sb.ToString()); + } + + private async Task<(bool, string)> HandleSelectingEngineAsync(EditOCRConfRedisHelper redis, string command) { + if (command == "1" || command == "PaddleOCR") { + await SetEngineAsync(OCREngine.PaddleOCR); + await redis.DeleteKeysAsync(); + return (true, "已切换到PaddleOCR引擎"); + } else if (command == "2" || command == "LLM") { + await SetEngineAsync(OCREngine.LLM); + await redis.SetStateAsync(OCRConfState.SelectingLLMChannel.GetDescription()); + + var channels = await GetAvailableLLMChannelsAsync(); + var sb = new StringBuilder(); + sb.AppendLine("已切换到LLM引擎"); + sb.AppendLine(); + sb.AppendLine("请选择LLM渠道(输入渠道ID):"); + foreach (var channel in channels) { + sb.AppendLine($"{channel.Id}. {channel.Name} ({channel.Provider})"); + } + return (true, sb.ToString()); + } + + return (true, "无效选择,请输入1或2"); + } + + private async Task<(bool, string)> HandleSelectingLLMChannelAsync(EditOCRConfRedisHelper redis, string command) { + if (int.TryParse(command, out var channelId)) { + var channel = await DataContext.LLMChannels + .FirstOrDefaultAsync(c => c.Id == channelId); + + if (channel != null) { + await redis.SetChannelIdAsync(channelId); + await redis.SetStateAsync(OCRConfState.SelectingLLMModel.GetDescription()); + + var models = await DataContext.ChannelsWithModel + .Where(m => m.LLMChannelId == channelId && !m.IsDeleted) + .Select(m => m.ModelName) + .ToListAsync(); + + var sb = new StringBuilder(); + sb.AppendLine($"已选择渠道: {channel.Name}"); + sb.AppendLine(); + sb.AppendLine("请输入OCR使用的模型名称:"); + if (models.Any()) { + sb.AppendLine("可选模型:"); + foreach (var model in models) { + sb.AppendLine($"- {model}"); + } + } + return (true, sb.ToString()); + } + } + + return (true, "无效的渠道ID,请重新输入"); + } + + private async Task<(bool, string)> HandleSelectingLLMModelAsync(EditOCRConfRedisHelper redis, string command) { + var channelId = await redis.GetChannelIdAsync(); + if (!channelId.HasValue) { + await redis.SetStateAsync(OCRConfState.SelectingLLMChannel.GetDescription()); + return (true, "请先选择LLM渠道"); + } + + await SetLLMConfigAsync(channelId.Value, command); + await redis.DeleteKeysAsync(); + + return (true, $"OCR配置完成!\n引擎: LLM\n渠道ID: {channelId}\n模型: {command}"); + } + + private async Task<(bool, string)> HandleViewingConfigAsync(EditOCRConfRedisHelper redis, string command) { + var sb = new StringBuilder(); + sb.AppendLine("📋 OCR配置详情"); + sb.AppendLine(); + sb.AppendLine($"引擎: {await GetCurrentEngineAsync()}"); + + var engineConfig = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == OCREngineKey); + + if (engineConfig?.Value == OCREngine.LLM.ToString()) { + var channelConfig = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == OCRLLMChannelIdKey); + var modelConfig = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == OCRLLMModelNameKey); + + if (channelConfig != null && int.TryParse(channelConfig.Value, out var channelId)) { + var channel = await DataContext.LLMChannels + .FirstOrDefaultAsync(c => c.Id == channelId); + sb.AppendLine($"LLM渠道: {channel?.Name ?? "未找到"} ({channelId})"); + } + + if (modelConfig != null) { + sb.AppendLine($"LLM模型: {modelConfig.Value}"); + } + } + + sb.AppendLine(); + sb.AppendLine("输入\"返回\"返回主菜单"); + + await redis.SetStateAsync(OCRConfState.MainMenu.GetDescription()); + return (true, sb.ToString()); + } + + private async Task GetCurrentEngineAsync() { + var config = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == OCREngineKey); + return config?.Value ?? OCREngine.PaddleOCR.ToString(); + } + + private async Task SetEngineAsync(OCREngine engine) { + var config = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == OCREngineKey); + + if (config == null) { + await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { + Key = OCREngineKey, + Value = engine.ToString() + }); + } else { + config.Value = engine.ToString(); + } + + await DataContext.SaveChangesAsync(); + } + + private async Task SetLLMConfigAsync(int channelId, string modelName) { + await SetConfigAsync(OCRLLMChannelIdKey, channelId.ToString()); + await SetConfigAsync(OCRLLMModelNameKey, modelName); + } + + private async Task SetConfigAsync(string key, string value) { + var config = await DataContext.AppConfigurationItems + .FirstOrDefaultAsync(x => x.Key == key); + + if (config == null) { + await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { + Key = key, + Value = value + }); + } else { + config.Value = value; + } + + await DataContext.SaveChangesAsync(); + } + + private async Task> GetAvailableLLMChannelsAsync() { + return await DataContext.LLMChannels.ToListAsync(); + } + } +} diff --git a/TelegramSearchBot/View/EditOCRConfView.cs b/TelegramSearchBot/View/EditOCRConfView.cs new file mode 100644 index 00000000..cfd2f95c --- /dev/null +++ b/TelegramSearchBot/View/EditOCRConfView.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Telegram.Bot; +using TelegramSearchBot.Interface; +using TelegramSearchBot.Service.BotAPI; + +namespace TelegramSearchBot.View { + public class EditOCRConfView : IView { + private readonly ISendMessageService _sendMessageService; + + private long _chatId; + private int _replyToMessageId; + private string _messageText; + + public EditOCRConfView(ISendMessageService sendMessageService) { + _sendMessageService = sendMessageService; + } + + public EditOCRConfView WithChatId(long chatId) { + _chatId = chatId; + return this; + } + + public EditOCRConfView WithReplyTo(int messageId) { + _replyToMessageId = messageId; + return this; + } + + public EditOCRConfView WithMessage(string message) { + _messageText = message; + return this; + } + + public async Task Render() { + await _sendMessageService.SplitAndSendTextMessage( + _messageText, + new Telegram.Bot.Types.Chat { Id = _chatId }, + _replyToMessageId); + } + } +} From 8505808e0cddb943bea395397f37fd8a60e42323 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 19 Mar 2026 12:19:22 +0800 Subject: [PATCH 4/5] chore: fix code formatting with dotnet format Fix whitespace formatting issues detected by CI on Windows --- .../Attributes/McpAttributes.cs | 3 +- .../Tools/IterationLimitReachedPayload.cs | 2 +- ...60303031828_AddUserWithGroupUniqueIndex.cs | 14 ++---- ...0313124507_AddChannelWithModelIsDeleted.cs | 14 ++---- .../Service/AI/LLM/GeneralLLMServiceTests.cs | 29 +++++++++--- .../AI/LLM/ModelCapabilityServiceTests.cs | 46 +++++++++++++------ ...OpenAIProviderHistorySerializationTests.cs | 2 +- .../Service/AI/LLM/GeminiService.cs | 2 +- .../Service/AI/LLM/McpToolHelper.cs | 9 ++-- .../Service/AI/LLM/OllamaService.cs | 2 +- .../Service/AI/LLM/OpenAIService.cs | 10 ++-- .../Service/Mcp/McpClient.cs | 2 +- .../Service/Mcp/McpServerManager.cs | 4 +- .../Service/Tools/FileToolService.cs | 6 +-- .../Helper/WordCloudHelperTests.cs | 4 +- .../Extension/ServiceCollectionExtension.cs | 2 +- .../BotAPI/SendMessageService.Streaming.cs | 2 +- .../Service/Storage/MessageService.cs | 10 ++-- .../Service/Vector/FaissVectorService.cs | 6 +-- 19 files changed, 97 insertions(+), 72 deletions(-) diff --git a/TelegramSearchBot.Common/Attributes/McpAttributes.cs b/TelegramSearchBot.Common/Attributes/McpAttributes.cs index 519b0001..9a383c34 100644 --- a/TelegramSearchBot.Common/Attributes/McpAttributes.cs +++ b/TelegramSearchBot.Common/Attributes/McpAttributes.cs @@ -1,7 +1,6 @@ using System; -namespace TelegramSearchBot.Attributes -{ +namespace TelegramSearchBot.Attributes { /// /// Marks a method as a tool that can be called by the LLM. /// Deprecated: Use instead for built-in tools. diff --git a/TelegramSearchBot.Common/Model/Tools/IterationLimitReachedPayload.cs b/TelegramSearchBot.Common/Model/Tools/IterationLimitReachedPayload.cs index b605ddef..4dec7584 100644 --- a/TelegramSearchBot.Common/Model/Tools/IterationLimitReachedPayload.cs +++ b/TelegramSearchBot.Common/Model/Tools/IterationLimitReachedPayload.cs @@ -22,7 +22,7 @@ public static bool IsIterationLimitMessage(string content) { /// 在累积内容末尾追加标记 /// public static string AppendMarker(string accumulatedContent) { - return (accumulatedContent ?? string.Empty) + Marker; + return ( accumulatedContent ?? string.Empty ) + Marker; } /// diff --git a/TelegramSearchBot.Database/Migrations/20260303031828_AddUserWithGroupUniqueIndex.cs b/TelegramSearchBot.Database/Migrations/20260303031828_AddUserWithGroupUniqueIndex.cs index 3378f31b..79ea689b 100644 --- a/TelegramSearchBot.Database/Migrations/20260303031828_AddUserWithGroupUniqueIndex.cs +++ b/TelegramSearchBot.Database/Migrations/20260303031828_AddUserWithGroupUniqueIndex.cs @@ -1,15 +1,12 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace TelegramSearchBot.Migrations -{ +namespace TelegramSearchBot.Migrations { /// - public partial class AddUserWithGroupUniqueIndex : Migration - { + public partial class AddUserWithGroupUniqueIndex : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateIndex( name: "IX_UsersWithGroup_UserId_GroupId", table: "UsersWithGroup", @@ -18,8 +15,7 @@ protected override void Up(MigrationBuilder migrationBuilder) } /// - protected override void Down(MigrationBuilder migrationBuilder) - { + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropIndex( name: "IX_UsersWithGroup_UserId_GroupId", table: "UsersWithGroup"); diff --git a/TelegramSearchBot.Database/Migrations/20260313124507_AddChannelWithModelIsDeleted.cs b/TelegramSearchBot.Database/Migrations/20260313124507_AddChannelWithModelIsDeleted.cs index 045e395e..ccd5f76c 100644 --- a/TelegramSearchBot.Database/Migrations/20260313124507_AddChannelWithModelIsDeleted.cs +++ b/TelegramSearchBot.Database/Migrations/20260313124507_AddChannelWithModelIsDeleted.cs @@ -1,15 +1,12 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace TelegramSearchBot.Migrations -{ +namespace TelegramSearchBot.Migrations { /// - public partial class AddChannelWithModelIsDeleted : Migration - { + public partial class AddChannelWithModelIsDeleted : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( name: "IsDeleted", table: "ChannelsWithModel", @@ -19,8 +16,7 @@ protected override void Up(MigrationBuilder migrationBuilder) } /// - protected override void Down(MigrationBuilder migrationBuilder) - { + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "IsDeleted", table: "ChannelsWithModel"); diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/GeneralLLMServiceTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/GeneralLLMServiceTests.cs index 52ec785b..87e3e109 100644 --- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/GeneralLLMServiceTests.cs +++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/GeneralLLMServiceTests.cs @@ -102,12 +102,20 @@ public async Task GetChannelsAsync_NoModels_ReturnsEmpty() { public async Task GetChannelsAsync_WithModel_ReturnsOrderedChannels() { // Arrange var channel1 = new LLMChannel { - Name = "ch1", Gateway = "gw1", ApiKey = "key1", - Provider = LLMProvider.OpenAI, Parallel = 2, Priority = 1 + Name = "ch1", + Gateway = "gw1", + ApiKey = "key1", + Provider = LLMProvider.OpenAI, + Parallel = 2, + Priority = 1 }; var channel2 = new LLMChannel { - Name = "ch2", Gateway = "gw2", ApiKey = "key2", - Provider = LLMProvider.OpenAI, Parallel = 3, Priority = 10 + Name = "ch2", + Gateway = "gw2", + ApiKey = "key2", + Provider = LLMProvider.OpenAI, + Parallel = 3, + Priority = 10 }; _dbContext.LLMChannels.AddRange(channel1, channel2); await _dbContext.SaveChangesAsync(); @@ -130,7 +138,10 @@ public async Task GetChannelsAsync_WithModel_ReturnsOrderedChannels() { public async Task ExecAsync_NoModelConfigured_YieldsNoResults() { // Arrange - no group settings configured var message = new TelegramSearchBot.Model.Data.Message { - Content = "test", GroupId = 123, MessageId = 1, FromUserId = 1 + Content = "test", + GroupId = 123, + MessageId = 1, + FromUserId = 1 }; // Act @@ -153,8 +164,12 @@ public async Task GetAvailableCapacityAsync_NoChannels_ReturnsZero() { public async Task GetAvailableCapacityAsync_WithChannels_ReturnsCapacity() { // Arrange var channel = new LLMChannel { - Name = "ch1", Gateway = "gw1", ApiKey = "key1", - Provider = LLMProvider.OpenAI, Parallel = 5, Priority = 1 + Name = "ch1", + Gateway = "gw1", + ApiKey = "key1", + Provider = LLMProvider.OpenAI, + Parallel = 5, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/ModelCapabilityServiceTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/ModelCapabilityServiceTests.cs index a649e8db..da4ef579 100644 --- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/ModelCapabilityServiceTests.cs +++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/ModelCapabilityServiceTests.cs @@ -67,8 +67,12 @@ public async Task GetModelCapabilities_NotFound_ReturnsNull() { public async Task GetModelCapabilities_WithCapabilities_ReturnsCorrectModel() { // Arrange var channel = new LLMChannel { - Name = "test", Gateway = "gw", ApiKey = "key", - Provider = LLMProvider.OpenAI, Parallel = 1, Priority = 1 + Name = "test", + Gateway = "gw", + ApiKey = "key", + Provider = LLMProvider.OpenAI, + Parallel = 1, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); @@ -106,8 +110,12 @@ public async Task GetModelCapabilities_WithCapabilities_ReturnsCorrectModel() { public async Task GetToolCallingSupportedModels_ReturnsCorrectModels() { // Arrange var channel = new LLMChannel { - Name = "test", Gateway = "gw", ApiKey = "key", - Provider = LLMProvider.OpenAI, Parallel = 1, Priority = 1 + Name = "test", + Gateway = "gw", + ApiKey = "key", + Provider = LLMProvider.OpenAI, + Parallel = 1, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); @@ -138,7 +146,7 @@ public async Task GetToolCallingSupportedModels_ReturnsCorrectModels() { await _dbContext.SaveChangesAsync(); // Act - var result = (await _service.GetToolCallingSupportedModels()).ToList(); + var result = ( await _service.GetToolCallingSupportedModels() ).ToList(); // Assert Assert.Single(result); @@ -149,8 +157,12 @@ public async Task GetToolCallingSupportedModels_ReturnsCorrectModels() { public async Task GetVisionSupportedModels_ReturnsCorrectModels() { // Arrange var channel = new LLMChannel { - Name = "test", Gateway = "gw", ApiKey = "key", - Provider = LLMProvider.OpenAI, Parallel = 1, Priority = 1 + Name = "test", + Gateway = "gw", + ApiKey = "key", + Provider = LLMProvider.OpenAI, + Parallel = 1, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); @@ -170,7 +182,7 @@ public async Task GetVisionSupportedModels_ReturnsCorrectModels() { await _dbContext.SaveChangesAsync(); // Act - var result = (await _service.GetVisionSupportedModels()).ToList(); + var result = ( await _service.GetVisionSupportedModels() ).ToList(); // Assert Assert.Single(result); @@ -181,8 +193,12 @@ public async Task GetVisionSupportedModels_ReturnsCorrectModels() { public async Task GetEmbeddingModels_ReturnsCorrectModels() { // Arrange var channel = new LLMChannel { - Name = "test", Gateway = "gw", ApiKey = "key", - Provider = LLMProvider.OpenAI, Parallel = 1, Priority = 1 + Name = "test", + Gateway = "gw", + ApiKey = "key", + Provider = LLMProvider.OpenAI, + Parallel = 1, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); @@ -202,7 +218,7 @@ public async Task GetEmbeddingModels_ReturnsCorrectModels() { await _dbContext.SaveChangesAsync(); // Act - var result = (await _service.GetEmbeddingModels()).ToList(); + var result = ( await _service.GetEmbeddingModels() ).ToList(); // Assert Assert.Single(result); @@ -213,8 +229,12 @@ public async Task GetEmbeddingModels_ReturnsCorrectModels() { public async Task CleanupOldCapabilities_RemovesOldEntries() { // Arrange var channel = new LLMChannel { - Name = "test", Gateway = "gw", ApiKey = "key", - Provider = LLMProvider.OpenAI, Parallel = 1, Priority = 1 + Name = "test", + Gateway = "gw", + ApiKey = "key", + Provider = LLMProvider.OpenAI, + Parallel = 1, + Priority = 1 }; _dbContext.LLMChannels.Add(channel); await _dbContext.SaveChangesAsync(); diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIProviderHistorySerializationTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIProviderHistorySerializationTests.cs index 3d5b003c..0529dbb2 100644 --- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIProviderHistorySerializationTests.cs +++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIProviderHistorySerializationTests.cs @@ -79,7 +79,7 @@ public void SerializeProviderHistory_WithToolCallHistory_PreservesContent() { }; var serialized = OpenAIService.SerializeProviderHistory(history); - + Assert.Equal(5, serialized.Count); Assert.Contains("tool_call", serialized[2].Content); Assert.Contains("bash", serialized[3].Content); diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs index b6c62aaf..2eecde60 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs @@ -341,7 +341,7 @@ public async IAsyncEnumerable ExecAsync( executionContext.IterationLimitReached = true; executionContext.SnapshotData = new LlmContinuationSnapshot { ChatId = ChatId, - OriginalMessageId = (int)message.MessageId, + OriginalMessageId = ( int ) message.MessageId, UserId = message.FromUserId, ModelName = modelName, Provider = "Gemini", diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs index e4595ad2..be5862d2 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs @@ -170,7 +170,7 @@ private static string RegisterToolsAndGetPromptString(List assemblies) var builtInParamAttr = param.GetCustomAttribute(); var mcpParamAttr = param.GetCustomAttribute(); var paramDescription = builtInParamAttr?.Description ?? mcpParamAttr?.Description ?? $"Parameter '{param.Name}'"; - var paramIsRequired = builtInParamAttr?.IsRequired ?? mcpParamAttr?.IsRequired ?? (!param.IsOptional && !param.HasDefaultValue); + var paramIsRequired = builtInParamAttr?.IsRequired ?? mcpParamAttr?.IsRequired ?? ( !param.IsOptional && !param.HasDefaultValue ); var paramType = MapToJsonSchemaType(param.ParameterType); properties[param.Name] = new Dictionary { @@ -511,7 +511,7 @@ private static (string toolName, Dictionary arguments) ParseTool } } - if (toolName == null || (!ToolRegistry.ContainsKey(toolName) && !ExternalToolRegistry.ContainsKey(toolName))) { + if (toolName == null || ( !ToolRegistry.ContainsKey(toolName) && !ExternalToolRegistry.ContainsKey(toolName) )) { _sLogger?.LogWarning($"ParseToolElement: Unregistered tool '{element.Name.LocalName}'"); return (null, null); } @@ -703,8 +703,7 @@ public static async Task ExecuteRegisteredToolAsync(string toolName, Dic if (result is Task taskResult) { await taskResult; - if (taskResult.GetType().IsGenericType) - { + if (taskResult.GetType().IsGenericType) { return ( ( dynamic ) taskResult ).Result; } return null; @@ -900,7 +899,7 @@ public static void RegisterExternalMcpTools(Interface.Mcp.IMcpServerManager mcpS RegisterExternalTools( toolInfos, async (serverName, toolName, arguments) => { - var objectArgs = arguments.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value); + var objectArgs = arguments.ToDictionary(kvp => kvp.Key, kvp => ( object ) kvp.Value); var result = await mcpServerManager.CallToolAsync(serverName, toolName, objectArgs); if (result.IsError) { return $"Error: {string.Join("\n", result.Content?.Select(c => c.Text ?? "") ?? Enumerable.Empty())}"; diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OllamaService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OllamaService.cs index e8a810e1..72d25c12 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/OllamaService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/OllamaService.cs @@ -202,7 +202,7 @@ public async IAsyncEnumerable ExecAsync(Model.Data.Message message, long executionContext.IterationLimitReached = true; executionContext.SnapshotData = new LlmContinuationSnapshot { ChatId = ChatId, - OriginalMessageId = (int)message.MessageId, + OriginalMessageId = ( int ) message.MessageId, UserId = message.FromUserId, ModelName = modelName, Provider = "Ollama", diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs index cf90a0eb..fb9a1a37 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs @@ -719,10 +719,10 @@ private static bool IsToolCallingNotSupportedError(Exception ex) { } } // Common error patterns when a model/API doesn't support tool calling - return message.Contains("tools", StringComparison.OrdinalIgnoreCase) && - (message.Contains("not supported", StringComparison.OrdinalIgnoreCase) || + return message.Contains("tools", StringComparison.OrdinalIgnoreCase) && + ( message.Contains("not supported", StringComparison.OrdinalIgnoreCase) || message.Contains("unsupported", StringComparison.OrdinalIgnoreCase) || - message.Contains("invalid", StringComparison.OrdinalIgnoreCase)) || + message.Contains("invalid", StringComparison.OrdinalIgnoreCase) ) || message.Contains("unrecognized request argument", StringComparison.OrdinalIgnoreCase); } @@ -860,7 +860,7 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync( executionContext.IterationLimitReached = true; executionContext.SnapshotData = new LlmContinuationSnapshot { ChatId = ChatId, - OriginalMessageId = (int)message.MessageId, + OriginalMessageId = ( int ) message.MessageId, UserId = message.FromUserId, ModelName = modelName, Provider = "OpenAI", @@ -967,7 +967,7 @@ private async IAsyncEnumerable ExecWithXmlToolCallingAsync( executionContext.IterationLimitReached = true; executionContext.SnapshotData = new LlmContinuationSnapshot { ChatId = ChatId, - OriginalMessageId = (int)message.MessageId, + OriginalMessageId = ( int ) message.MessageId, UserId = message.FromUserId, ModelName = modelName, Provider = "OpenAI", diff --git a/TelegramSearchBot.LLM/Service/Mcp/McpClient.cs b/TelegramSearchBot.LLM/Service/Mcp/McpClient.cs index f36c45b5..8cf2a9e9 100644 --- a/TelegramSearchBot.LLM/Service/Mcp/McpClient.cs +++ b/TelegramSearchBot.LLM/Service/Mcp/McpClient.cs @@ -179,7 +179,7 @@ private async Task CleanupProcessAsync() { _process.Kill(true); // Wait briefly for the process to actually exit try { - _process.WaitForExit(ProcessExitTimeoutMs); + _process.WaitForExit(ProcessExitTimeoutMs); } catch { } } } catch { } diff --git a/TelegramSearchBot.LLM/Service/Mcp/McpServerManager.cs b/TelegramSearchBot.LLM/Service/Mcp/McpServerManager.cs index d93a8a05..fa4f2cdb 100644 --- a/TelegramSearchBot.LLM/Service/Mcp/McpServerManager.cs +++ b/TelegramSearchBot.LLM/Service/Mcp/McpServerManager.cs @@ -152,7 +152,7 @@ private async Task DisconnectServerAsync(string serverName) { if (_clients.TryRemove(serverName, out var client)) { try { await client.DisconnectAsync(); - (client as IDisposable)?.Dispose(); + ( client as IDisposable )?.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "Error disconnecting MCP server '{Name}'", serverName); } @@ -293,7 +293,7 @@ public async Task ShutdownAllAsync() { public void Dispose() { foreach (var kvp in _clients) { try { - (kvp.Value as IDisposable)?.Dispose(); + ( kvp.Value as IDisposable )?.Dispose(); } catch { } } _clients.Clear(); diff --git a/TelegramSearchBot.LLM/Service/Tools/FileToolService.cs b/TelegramSearchBot.LLM/Service/Tools/FileToolService.cs index 4c9a517d..8479e9ff 100644 --- a/TelegramSearchBot.LLM/Service/Tools/FileToolService.cs +++ b/TelegramSearchBot.LLM/Service/Tools/FileToolService.cs @@ -286,7 +286,7 @@ private static string ResolvePath(string path) { private static int CountOccurrences(string text, string pattern) { int count = 0; int index = 0; - while ((index = text.IndexOf(pattern, index, StringComparison.Ordinal)) != -1) { + while (( index = text.IndexOf(pattern, index, StringComparison.Ordinal) ) != -1) { count++; index += pattern.Length; } @@ -296,8 +296,8 @@ private static int CountOccurrences(string text, string pattern) { private static string FormatFileSize(long bytes) { if (bytes < 1024) return $"{bytes}B"; if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1}KB"; - if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1}MB"; - return $"{bytes / (1024.0 * 1024 * 1024):F1}GB"; + if (bytes < 1024 * 1024 * 1024) return $"{bytes / ( 1024.0 * 1024 ):F1}MB"; + return $"{bytes / ( 1024.0 * 1024 * 1024 ):F1}GB"; } } } diff --git a/TelegramSearchBot.Test/Helper/WordCloudHelperTests.cs b/TelegramSearchBot.Test/Helper/WordCloudHelperTests.cs index bbfd566e..a4eac7ac 100644 --- a/TelegramSearchBot.Test/Helper/WordCloudHelperTests.cs +++ b/TelegramSearchBot.Test/Helper/WordCloudHelperTests.cs @@ -33,8 +33,8 @@ public void GenerateWordCloud_ShouldCreateImageFile() { // 打印文件路径 System.Console.WriteLine($"词云图片已保存到: {Path.GetFullPath(outputPath)}"); } catch (Exception ex) when (ex is System.DllNotFoundException || - (ex is System.TypeInitializationException tex && - tex.InnerException is System.PlatformNotSupportedException or System.DllNotFoundException)) { + ( ex is System.TypeInitializationException tex && + tex.InnerException is System.PlatformNotSupportedException or System.DllNotFoundException )) { // 在Linux上GDI+不可用时跳过测试(包括libgdiplus未安装或平台不支持) System.Console.WriteLine($"跳过测试:{ex.Message}"); return; diff --git a/TelegramSearchBot/Extension/ServiceCollectionExtension.cs b/TelegramSearchBot/Extension/ServiceCollectionExtension.cs index 409dedbd..f7135663 100644 --- a/TelegramSearchBot/Extension/ServiceCollectionExtension.cs +++ b/TelegramSearchBot/Extension/ServiceCollectionExtension.cs @@ -22,11 +22,11 @@ using TelegramSearchBot.Executor; using TelegramSearchBot.Helper; using TelegramSearchBot.Interface; +using TelegramSearchBot.Interface.AI.LLM; using TelegramSearchBot.Interface.Controller; using TelegramSearchBot.Manager; using TelegramSearchBot.Model; using TelegramSearchBot.Search.Tool; -using TelegramSearchBot.Interface.AI.LLM; using TelegramSearchBot.Service.BotAPI; using TelegramSearchBot.Service.Storage; using TelegramSearchBot.View; diff --git a/TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs b/TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs index 605949f1..f32db4df 100644 --- a/TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs +++ b/TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs @@ -359,7 +359,7 @@ await botClient.SendMessage( } // Generate a unique draftId to avoid collisions for concurrent requests to the same message - int draftId = unchecked((int)(chatId ^ replyTo ^ DateTime.UtcNow.Ticks)); + int draftId = unchecked(( int ) ( chatId ^ replyTo ^ DateTime.UtcNow.Ticks )); string latestContent = null; bool draftStarted = false; diff --git a/TelegramSearchBot/Service/Storage/MessageService.cs b/TelegramSearchBot/Service/Storage/MessageService.cs index 39051747..2fc829a9 100644 --- a/TelegramSearchBot/Service/Storage/MessageService.cs +++ b/TelegramSearchBot/Service/Storage/MessageService.cs @@ -54,7 +54,7 @@ public async Task AddToSqlite(MessageOption messageOption) { var existingUserInGroup = await DataContext.UsersWithGroup .FirstOrDefaultAsync(s => s.UserId == messageOption.UserId && s.GroupId == messageOption.ChatId); - + if (existingUserInGroup == null) { // 使用 try-catch 处理并发插入导致的唯一约束冲突 try { @@ -75,7 +75,7 @@ await DataContext.UsersWithGroup.AddAsync(new UserWithGroup() { var existingUserData = await DataContext.UserData .FirstOrDefaultAsync(s => s.Id == messageOption.User.Id); - + if (existingUserData == null && messageOption.User != null) { try { await DataContext.UserData.AddAsync(new UserData() { @@ -96,7 +96,7 @@ await DataContext.UserData.AddAsync(new UserData() { var existingGroupData = await DataContext.GroupData .FirstOrDefaultAsync(s => s.Id == messageOption.Chat.Id); - + if (existingGroupData == null && messageOption.Chat != null) { try { await DataContext.GroupData.AddAsync(new GroupData() { @@ -112,7 +112,7 @@ await DataContext.GroupData.AddAsync(new GroupData() { DataContext.ChangeTracker.Clear(); } } - + var message = new Message() { GroupId = messageOption.ChatId, MessageId = messageOption.MessageId, @@ -123,7 +123,7 @@ await DataContext.GroupData.AddAsync(new GroupData() { if (messageOption.ReplyTo != 0) { message.ReplyToMessageId = messageOption.ReplyTo; } - + await DataContext.Messages.AddAsync(message); await DataContext.SaveChangesAsync(); return message.Id; diff --git a/TelegramSearchBot/Service/Vector/FaissVectorService.cs b/TelegramSearchBot/Service/Vector/FaissVectorService.cs index 24512ab0..035199af 100644 --- a/TelegramSearchBot/Service/Vector/FaissVectorService.cs +++ b/TelegramSearchBot/Service/Vector/FaissVectorService.cs @@ -75,7 +75,7 @@ private async Task CheckEmbeddingModelAvailabilityAsync() { try { // 尝试生成一个简单的测试向量 var testVector = await _generalLLMService.GenerateEmbeddingsAsync("test", CancellationToken.None); - + if (testVector == null || testVector.Length == 0) { _logger.LogWarning("嵌入模型不可用或返回空向量,已禁用FAISS向量服务"); _isEnabled = false; @@ -680,14 +680,14 @@ private static void SetSegmentVectorizedStatus(DataDbContext dbContext, Conversa public async Task GenerateVectorAsync(string text) { try { var vector = await _generalLLMService.GenerateEmbeddingsAsync(text); - + // 如果返回空向量,记录警告并禁用服务 if (vector == null || vector.Length == 0) { _logger.LogWarning("嵌入模型返回空向量,FAISS向量服务已禁用"); _isEnabled = false; return Array.Empty(); } - + return vector; } catch (Exception ex) { _logger.LogWarning(ex, "生成向量时出错,FAISS向量服务已禁用"); From 0ce76acf770f2105c73a7c1e2c68f4f94cc82aec Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 19 Mar 2026 12:21:27 +0800 Subject: [PATCH 5/5] fix: code formatting for OCR feature --- .../Controller/AI/OCR/AutoOCRController.cs | 6 ++-- TelegramSearchBot/Model/AI/OCRConfState.cs | 2 +- .../Service/AI/OCR/LLMOCRService.cs | 4 +-- .../Service/Manage/EditOCRConfService.cs | 32 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs b/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs index dd77b63c..054c11f1 100644 --- a/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs +++ b/TelegramSearchBot/Controller/AI/OCR/AutoOCRController.cs @@ -70,11 +70,11 @@ public async Task ExecuteAsync(PipelineContext p) { try { var PhotoStream = await IProcessPhoto.GetPhoto(e); logger.LogInformation($"Get Photo File: {e.Message.Chat.Id}/{e.Message.MessageId}"); - + var engine = await GetOCREngineAsync(); - var ocrService = _ocrServices.FirstOrDefault(s => s.Engine == engine) + var ocrService = _ocrServices.FirstOrDefault(s => s.Engine == engine) ?? _ocrServices.First(s => s.Engine == OCREngine.PaddleOCR); - + logger.LogInformation($"使用OCR引擎: {engine}"); OcrStr = await ocrService.ExecuteAsync(new MemoryStream(PhotoStream)); if (!string.IsNullOrWhiteSpace(OcrStr)) { diff --git a/TelegramSearchBot/Model/AI/OCRConfState.cs b/TelegramSearchBot/Model/AI/OCRConfState.cs index a97a289a..1605adcb 100644 --- a/TelegramSearchBot/Model/AI/OCRConfState.cs +++ b/TelegramSearchBot/Model/AI/OCRConfState.cs @@ -21,7 +21,7 @@ public enum OCRConfState { public static class OCRConfStateExtensions { public static string GetDescription(this OCRConfState state) { var fieldInfo = state.GetType().GetField(state.ToString()); - var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + var attributes = ( DescriptionAttribute[] ) fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); return attributes.Length > 0 ? attributes[0].Description : state.ToString(); } } diff --git a/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs b/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs index cd1b018d..a7b06ba6 100644 --- a/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs +++ b/TelegramSearchBot/Service/AI/OCR/LLMOCRService.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.Logging; using SkiaSharp; using TelegramSearchBot.Attributes; -using TelegramSearchBot.Interface.AI.OCR; using TelegramSearchBot.Interface.AI.LLM; +using TelegramSearchBot.Interface.AI.OCR; namespace TelegramSearchBot.Service.AI.OCR { [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)] @@ -28,7 +28,7 @@ public async Task ExecuteAsync(Stream file) { var tg_img = SKBitmap.Decode(file); var tg_img_data = tg_img.Encode(SKEncodedImageFormat.Jpeg, 99); var tempPath = Path.GetTempFileName() + ".jpg"; - + try { using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { tg_img_data.SaveTo(fs); diff --git a/TelegramSearchBot/Service/Manage/EditOCRConfService.cs b/TelegramSearchBot/Service/Manage/EditOCRConfService.cs index fdf5f565..aeb1296f 100644 --- a/TelegramSearchBot/Service/Manage/EditOCRConfService.cs +++ b/TelegramSearchBot/Service/Manage/EditOCRConfService.cs @@ -50,7 +50,7 @@ ILogger logger public async Task<(bool, string)> ExecuteAsync(string command, long chatId) { var redis = new EditOCRConfRedisHelper(connectionMultiplexer, chatId); - + var currentState = await redis.GetStateAsync(); if (string.IsNullOrEmpty(currentState)) { currentState = OCRConfState.MainMenu.GetDescription(); @@ -78,7 +78,7 @@ ILogger logger var sb = new StringBuilder(); sb.AppendLine("🔧 OCR配置"); sb.AppendLine(); - + var currentEngine = await GetCurrentEngineAsync(); sb.AppendLine($"当前OCR引擎: {currentEngine}"); sb.AppendLine(); @@ -98,7 +98,7 @@ ILogger logger } else if (command == "2" || command == "LLM") { await SetEngineAsync(OCREngine.LLM); await redis.SetStateAsync(OCRConfState.SelectingLLMChannel.GetDescription()); - + var channels = await GetAvailableLLMChannelsAsync(); var sb = new StringBuilder(); sb.AppendLine("已切换到LLM引擎"); @@ -117,16 +117,16 @@ ILogger logger if (int.TryParse(command, out var channelId)) { var channel = await DataContext.LLMChannels .FirstOrDefaultAsync(c => c.Id == channelId); - + if (channel != null) { await redis.SetChannelIdAsync(channelId); await redis.SetStateAsync(OCRConfState.SelectingLLMModel.GetDescription()); - + var models = await DataContext.ChannelsWithModel .Where(m => m.LLMChannelId == channelId && !m.IsDeleted) .Select(m => m.ModelName) .ToListAsync(); - + var sb = new StringBuilder(); sb.AppendLine($"已选择渠道: {channel.Name}"); sb.AppendLine(); @@ -153,7 +153,7 @@ ILogger logger await SetLLMConfigAsync(channelId.Value, command); await redis.DeleteKeysAsync(); - + return (true, $"OCR配置完成!\n引擎: LLM\n渠道ID: {channelId}\n模型: {command}"); } @@ -162,27 +162,27 @@ ILogger logger sb.AppendLine("📋 OCR配置详情"); sb.AppendLine(); sb.AppendLine($"引擎: {await GetCurrentEngineAsync()}"); - + var engineConfig = await DataContext.AppConfigurationItems .FirstOrDefaultAsync(x => x.Key == OCREngineKey); - + if (engineConfig?.Value == OCREngine.LLM.ToString()) { var channelConfig = await DataContext.AppConfigurationItems .FirstOrDefaultAsync(x => x.Key == OCRLLMChannelIdKey); var modelConfig = await DataContext.AppConfigurationItems .FirstOrDefaultAsync(x => x.Key == OCRLLMModelNameKey); - + if (channelConfig != null && int.TryParse(channelConfig.Value, out var channelId)) { var channel = await DataContext.LLMChannels .FirstOrDefaultAsync(c => c.Id == channelId); sb.AppendLine($"LLM渠道: {channel?.Name ?? "未找到"} ({channelId})"); } - + if (modelConfig != null) { sb.AppendLine($"LLM模型: {modelConfig.Value}"); } } - + sb.AppendLine(); sb.AppendLine("输入\"返回\"返回主菜单"); @@ -199,7 +199,7 @@ private async Task GetCurrentEngineAsync() { private async Task SetEngineAsync(OCREngine engine) { var config = await DataContext.AppConfigurationItems .FirstOrDefaultAsync(x => x.Key == OCREngineKey); - + if (config == null) { await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { Key = OCREngineKey, @@ -208,7 +208,7 @@ await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { } else { config.Value = engine.ToString(); } - + await DataContext.SaveChangesAsync(); } @@ -220,7 +220,7 @@ private async Task SetLLMConfigAsync(int channelId, string modelName) { private async Task SetConfigAsync(string key, string value) { var config = await DataContext.AppConfigurationItems .FirstOrDefaultAsync(x => x.Key == key); - + if (config == null) { await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { Key = key, @@ -229,7 +229,7 @@ await DataContext.AppConfigurationItems.AddAsync(new AppConfigurationItem { } else { config.Value = value; } - + await DataContext.SaveChangesAsync(); }