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.Common/Model/Tools/SendDocumentResult.cs b/TelegramSearchBot.Common/Model/Tools/SendDocumentResult.cs
new file mode 100644
index 00000000..1a770e3e
--- /dev/null
+++ b/TelegramSearchBot.Common/Model/Tools/SendDocumentResult.cs
@@ -0,0 +1,8 @@
+namespace TelegramSearchBot.Model.Tools {
+ public class SendDocumentResult {
+ public bool Success { get; set; }
+ public int? MessageId { get; set; }
+ public long ChatId { get; set; }
+ public string Error { get; set; }
+ }
+}
diff --git a/TelegramSearchBot.Common/Model/Tools/SendVideoResult.cs b/TelegramSearchBot.Common/Model/Tools/SendVideoResult.cs
new file mode 100644
index 00000000..921808a4
--- /dev/null
+++ b/TelegramSearchBot.Common/Model/Tools/SendVideoResult.cs
@@ -0,0 +1,8 @@
+namespace TelegramSearchBot.Model.Tools {
+ public class SendVideoResult {
+ public bool Success { get; set; }
+ public int? MessageId { get; set; }
+ public long ChatId { get; set; }
+ public string Error { get; set; }
+ }
+}
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/McpToolHelper.cs b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
index 402e9de9..9abe16ea 100644
--- a/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
+++ b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
@@ -177,7 +177,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 {
@@ -518,7 +518,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);
}
@@ -713,8 +713,7 @@ public static async Task
private static ReplyParameters GetReplyParameters(long? explicitReplyToMessageId, ToolContext toolContext) {
- var messageId = explicitReplyToMessageId ?? (toolContext.MessageId != 0 ? toolContext.MessageId : (long?)null);
- return messageId.HasValue ? new ReplyParameters { MessageId = messageId.Value } : null;
+ long? messageId = explicitReplyToMessageId ?? ( toolContext.MessageId != 0 ? toolContext.MessageId : ( long? ) null );
+ return messageId.HasValue ? new ReplyParameters { MessageId = ( int ) messageId.Value } : null;
}
[BuiltInTool("Sends a photo to the current chat using base64 encoded image data.", Name = "send_photo_base64")]
diff --git a/TelegramSearchBot/Service/Tools/SendVideoToolService.cs b/TelegramSearchBot/Service/Tools/SendVideoToolService.cs
new file mode 100644
index 00000000..b5e1d373
--- /dev/null
+++ b/TelegramSearchBot/Service/Tools/SendVideoToolService.cs
@@ -0,0 +1,104 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Telegram.Bot;
+using Telegram.Bot.Types;
+using Telegram.Bot.Types.Enums;
+using TelegramSearchBot.Attributes;
+using TelegramSearchBot.Common;
+using TelegramSearchBot.Interface;
+using TelegramSearchBot.Interface.Tools;
+using TelegramSearchBot.Manager;
+using TelegramSearchBot.Model;
+using TelegramSearchBot.Model.Tools;
+
+namespace TelegramSearchBot.Service.Tools {
+ [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient)]
+ public class SendVideoToolService : IService, ISendVideoToolService {
+ public string ServiceName => "SendVideoToolService";
+
+ private readonly ITelegramBotClient _botClient;
+ private readonly SendMessage _sendMessage;
+
+ private static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(120);
+
+ public SendVideoToolService(ITelegramBotClient botClient, SendMessage sendMessage) {
+ _botClient = botClient;
+ _sendMessage = sendMessage;
+ }
+
+ ///
+ /// Resolves the effective reply-to message ID. Uses the explicitly provided value if set,
+ /// otherwise falls back to the ToolContext.MessageId (the original user message).
+ ///
+ private static ReplyParameters GetReplyParameters(long? explicitReplyToMessageId, ToolContext toolContext) {
+ long? messageId = explicitReplyToMessageId ?? ( toolContext.MessageId != 0 ? toolContext.MessageId : ( long? ) null );
+ return messageId.HasValue ? new ReplyParameters { MessageId = ( int ) messageId.Value } : null;
+ }
+
+ [BuiltInTool("Sends a video to the current chat using a file path.", Name = "send_video_file")]
+ public async Task SendVideoFile(
+ [BuiltInParameter("The file path to the video on the server.")] string filePath,
+ ToolContext toolContext,
+ [BuiltInParameter("Optional caption for the video (max 1024 characters).", IsRequired = false)] string caption = null,
+ [BuiltInParameter("Optional message ID to reply to.", IsRequired = false)] long? replyToMessageId = null) {
+ try {
+ if (string.IsNullOrWhiteSpace(filePath)) {
+ return new SendVideoResult {
+ Success = false,
+ ChatId = toolContext.ChatId,
+ Error = "File path cannot be empty."
+ };
+ }
+
+ if (!File.Exists(filePath)) {
+ return new SendVideoResult {
+ Success = false,
+ ChatId = toolContext.ChatId,
+ Error = $"File not found: {filePath}"
+ };
+ }
+
+ var fileInfo = new FileInfo(filePath);
+ long maxFileSizeBytes = Env.IsLocalAPI
+ ? 2L * 1024 * 1024 * 1024 // Local API: 2GB
+ : 50 * 1024 * 1024; // Bot API: 50MB
+ if (fileInfo.Length > maxFileSizeBytes) {
+ return new SendVideoResult {
+ Success = false,
+ ChatId = toolContext.ChatId,
+ Error = $"File is too large ({fileInfo.Length / 1024 / 1024}MB). Maximum allowed size is {( Env.IsLocalAPI ? "2GB" : "50MB" )}."
+ };
+ }
+
+ var fileBytes = await File.ReadAllBytesAsync(filePath);
+ var video = InputFile.FromStream(new MemoryStream(fileBytes), fileInfo.Name);
+
+ var replyParameters = GetReplyParameters(replyToMessageId, toolContext);
+
+ using var cts = new CancellationTokenSource(SendTimeout);
+ var message = await _sendMessage.AddTaskWithResult(async () => await _botClient.SendVideo(
+ chatId: toolContext.ChatId,
+ video: video,
+ caption: string.IsNullOrEmpty(caption) ? null : caption.Length > 1024 ? caption.Substring(0, 1024) : caption,
+ parseMode: ParseMode.Html,
+ replyParameters: replyParameters,
+ cancellationToken: cts.Token
+ ), toolContext.ChatId);
+
+ return new SendVideoResult {
+ Success = true,
+ MessageId = message.MessageId,
+ ChatId = message.Chat.Id
+ };
+ } catch (Exception ex) {
+ return new SendVideoResult {
+ Success = false,
+ ChatId = toolContext.ChatId,
+ Error = $"Failed to send video: {ex.Message}"
+ };
+ }
+ }
+ }
+}
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向量服务已禁用");