diff --git a/Docs/Bot_Commands_User_Guide.md b/Docs/Bot_Commands_User_Guide.md index 6bb83bda..34f60ef7 100644 --- a/Docs/Bot_Commands_User_Guide.md +++ b/Docs/Bot_Commands_User_Guide.md @@ -213,7 +213,7 @@ 6. 机器人回复: `请输入没有群级配置时使用的默认生图模型名称...` 7. 管理员发送: `gpt-image-2`,或 MiniMax 的 `image-01` / `image-01-live` 8. 机器人回复: `默认生图模型已设置为: gpt-image-2...` - * **说明**: 生图工具使用内置工具 `generate_image`,默认优先使用当前群通过 `设置生图模型 <模型名>` 或 `选择生图模型` 配置的模型;群内未配置时使用全局默认 `gpt-image-2`。OpenAI-compatible 模型调用 `/v1/images/generations`;MiniMax `image-01` / `image-01-live` 调用 `/v1/image_generation`。API 地址和 API Key 不在工具参数中填写,而是来自该模型关联的 LLM 渠道;因此可通过 `新建渠道` / `编辑渠道` 自定义 API 地址,例如 `https://api.openai.com/v1`、`https://api.minimaxi.com` 或自建兼容网关。 + * **说明**: 生图工具使用内置工具 `generate_image`,默认优先使用当前群通过 `设置生图模型 <模型名>` 或 `选择生图模型` 配置的模型;群内未配置时使用全局默认 `gpt-image-2`。OpenAI-compatible 模型调用 `/v1/images/generations`;MiniMax `image-01` / `image-01-live` 调用 `/v1/image_generation`。API 地址和 API Key 不在工具参数中填写,而是来自该模型关联的 LLM 渠道;因此可通过 `新建渠道` / `编辑渠道` 自定义 API 地址,例如 `https://api.openai.com/v1`、`https://api.minimaxi.com` 或自建兼容网关。生图发送到群内时默认会同时发送 Telegram photo 预览和原图 document 文件。 * **音乐工具开关与模型设置:** 1. 管理员发送: `开启音乐工具` 2. 机器人回复: `音乐工具已开启...` diff --git a/Docs/README_MCP.md b/Docs/README_MCP.md index 89f0b04a..ad0aa654 100644 --- a/Docs/README_MCP.md +++ b/Docs/README_MCP.md @@ -54,7 +54,7 @@ TelegramSearchBot 内置了以下工具,通过 `BuiltInToolAttribute` 标记 | 工具名称 | 描述 | 参数 | |---------|------|------| -| `generate_image` | 通过配置的图片 API 生成图片,默认使用当前群的生图模型;未配置时使用全局默认 `gpt-image-2`;OpenAI-compatible 模型走 `/v1/images/generations`,MiniMax `image-01` / `image-01-live` 走 `/v1/image_generation`;图片保存到本地并可直接发送到当前聊天 | `prompt`, `model?`, `size?`, `quality?`, `output_format?`, `background?`, `moderation?`, `aspect_ratio?`, `minimax_response_format?`, `seed?`, `prompt_optimizer?`, `aigc_watermark?`, `style_type?`, `style_weight?`, `count?`, `send_to_chat?`, `caption?`, `reply_to_message_id?`, `timeout_seconds?` | +| `generate_image` | 通过配置的图片 API 生成图片,默认使用当前群的生图模型;未配置时使用全局默认 `gpt-image-2`;OpenAI-compatible 模型走 `/v1/images/generations`,MiniMax `image-01` / `image-01-live` 走 `/v1/image_generation`;图片保存到本地并可直接发送到当前聊天,默认同时发送 Telegram photo 预览和原图 document 文件 | `prompt`, `model?`, `size?`, `quality?`, `output_format?`, `background?`, `moderation?`, `aspect_ratio?`, `minimax_response_format?`, `seed?`, `prompt_optimizer?`, `aigc_watermark?`, `style_type?`, `style_weight?`, `count?`, `send_to_chat?`, `send_original_file?`, `caption?`, `reply_to_message_id?`, `timeout_seconds?` | 管理员命令: - `开启生图工具`:允许 `generate_image` 注入到 LLM 工具提示词和 native tool definitions。 diff --git a/README.md b/README.md index d0e83dc9..7b10f829 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - Anthropic Claude API - 可配置多模型通道管理 - **MCP (Model Context Protocol) 工具支持** - - **生图工具**: 内置 `generate_image`,按当前群配置选择生图模型,未配置时默认 `gpt-image-2`,支持 OpenAI-compatible Image API 与 MiniMax `image-01` / `image-01-live`,API 地址可通过 LLM 渠道自定义 + - **生图工具**: 内置 `generate_image`,按当前群配置选择生图模型,未配置时默认 `gpt-image-2`,支持 OpenAI-compatible Image API 与 MiniMax `image-01` / `image-01-live`,API 地址可通过 LLM 渠道自定义,默认同时发送图片预览和原图文件 5. 高级功能: - 短链接映射服务 - 消息扩展存储 @@ -160,7 +160,7 @@ ## MCP (Model Context Protocol) 支持 通过MCP协议扩展机器人能力,支持外部工具服务器: - ✅ **内置工具**: 发送文件、搜索、URL处理等24+内置工具 -- ✅ **内置生图**: 管理员可用 `开启生图工具` / `关闭生图工具` 控制是否向 LLM 注入 `generate_image`,群内可用 `选择生图模型` 或 `设置生图模型 <模型名>` 配置当前群生图模型,未配置时使用全局默认 `gpt-image-2` +- ✅ **内置生图**: 管理员可用 `开启生图工具` / `关闭生图工具` 控制是否向 LLM 注入 `generate_image`,群内可用 `选择生图模型` 或 `设置生图模型 <模型名>` 配置当前群生图模型,未配置时使用全局默认 `gpt-image-2`;默认同时发送图片预览和原图文件 - ✅ **外部MCP服务器**: 可动态添加第三方MCP服务器 - ✅ **管理员管理**: 通过指令管理MCP服务器(`新建渠道`等) diff --git a/TelegramSearchBot.Common/Model/Tools/ImageGenerationResult.cs b/TelegramSearchBot.Common/Model/Tools/ImageGenerationResult.cs index 0ebd1888..8d80a96e 100644 --- a/TelegramSearchBot.Common/Model/Tools/ImageGenerationResult.cs +++ b/TelegramSearchBot.Common/Model/Tools/ImageGenerationResult.cs @@ -10,6 +10,7 @@ public class ImageGenerationResult { public string Endpoint { get; set; } = null!; public List Images { get; set; } = new(); public List SentPhotos { get; set; } = new(); + public List SentOriginalFiles { get; set; } = new(); } public class GeneratedImageInfo { diff --git a/TelegramSearchBot/Service/Tools/ImageGenerationToolService.cs b/TelegramSearchBot/Service/Tools/ImageGenerationToolService.cs index 3d7bacd6..aad0fe21 100644 --- a/TelegramSearchBot/Service/Tools/ImageGenerationToolService.cs +++ b/TelegramSearchBot/Service/Tools/ImageGenerationToolService.cs @@ -221,6 +221,7 @@ public class ImageGenerationToolService : IService { private const int MaxImageCount = 9; private const string DefaultMiniMaxResponseFormat = "base64"; private const long TelegramPhotoLimitBytes = 10 * 1024 * 1024; + private static long TelegramDocumentLimitBytes => Env.IsLocalAPI ? 2L * 1024 * 1024 * 1024 : 50L * 1024 * 1024; private static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(120); private static readonly string[] MiniMaxImageModels = { "image-01", "image-01-live" }; private static readonly string[] MiniMaxAspectRatios = { "1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9" }; @@ -253,9 +254,9 @@ public ImageGenerationToolService( _logger = logger; } - [BuiltInTool(@"Generate an image through the configured image API and optionally send it to the current Telegram chat. +[BuiltInTool(@"Generate an image through the configured image API and optionally send it to the current Telegram chat. The default model is the current chat's configured image generation model, falling back to the bot-wide default gpt-image-2 when the chat has no image model configured. OpenAI-compatible models use /v1/images/generations. MiniMax image-01 and image-01-live use /v1/image_generation. The API base URL and API key are read from the configured LLM channel for the selected image model, so administrators can use OpenAI, MiniMax, or a compatible custom endpoint. -Use this when the user asks you to draw, create, render, generate, or revise an image. The tool saves generated image files under the bot work directory and returns their file paths.", Name = ToolName)] +Use this when the user asks you to draw, create, render, generate, or revise an image. The tool saves generated image files under the bot work directory and returns their file paths. When sending to chat, it sends a Telegram photo preview and, by default, the original saved image file as a document.", Name = ToolName)] public async Task GenerateImage( [BuiltInParameter("Image prompt. Be specific about subject, style, composition, lighting, colors, and any text that should appear.")] string prompt, ToolContext toolContext, @@ -274,6 +275,7 @@ public async Task GenerateImage( [BuiltInParameter("MiniMax image-01-live style_weight in (0, 1]. Defaults to MiniMax service default when empty.", IsRequired = false)] double? styleWeight = null, [BuiltInParameter("Number of images to generate, from 1 to 9. Defaults to 1.", IsRequired = false)] int count = 1, [BuiltInParameter("Whether to send generated images to the current Telegram chat. Defaults to true.", IsRequired = false)] bool sendToChat = true, + [BuiltInParameter("Whether to also send the original generated image files as Telegram documents. Defaults to true. Only applies when sendToChat is true.", IsRequired = false)] bool sendOriginalFile = true, [BuiltInParameter("Optional Telegram photo caption. Keep it short.", IsRequired = false)] string caption = null, [BuiltInParameter("Optional Telegram message ID to reply to. Defaults to the original user message.", IsRequired = false)] long? replyToMessageId = null, [BuiltInParameter("Request timeout in seconds, from 30 to 600. Defaults to 300.", IsRequired = false)] int timeoutSeconds = 300) { @@ -358,18 +360,28 @@ public async Task GenerateImage( if (sendToChat) { foreach (var image in generated) { - result.SentPhotos.Add(await SendGeneratedImageAsync( + var fileName = Path.GetFileName(image.Info.FilePath); + result.SentPhotos.Add(await SendGeneratedImagePhotoAsync( image.Bytes, - Path.GetFileName(image.Info.FilePath), + fileName, toolContext, caption, replyToMessageId)); + + if (sendOriginalFile) { + result.SentOriginalFiles.Add(await SendGeneratedImageDocumentAsync( + image.Bytes, + fileName, + toolContext, + replyToMessageId)); + } } } result.Success = result.Images.Count > 0; - if (result.Success && result.SentPhotos.Any(x => !x.Success)) { - result.Error = "Generated image files were saved, but one or more Telegram photo sends failed."; + if (result.Success && + ( result.SentPhotos.Any(x => !x.Success) || result.SentOriginalFiles.Any(x => !x.Success) )) { + result.Error = "Generated image files were saved, but one or more Telegram sends failed."; } return result; @@ -648,7 +660,7 @@ private async Task SaveImageAsync( }); } - private async Task SendGeneratedImageAsync( + private async Task SendGeneratedImagePhotoAsync( byte[] imageBytes, string fileName, ToolContext toolContext, @@ -699,6 +711,52 @@ private async Task SendGeneratedImageAsync( } } + private async Task SendGeneratedImageDocumentAsync( + byte[] imageBytes, + string fileName, + ToolContext toolContext, + long? replyToMessageId) { + try { + if (toolContext == null || toolContext.ChatId == 0) { + return new SendDocumentResult { + Success = false, + Error = "Cannot send original generated image because chat context is missing." + }; + } + + if (imageBytes.LongLength > TelegramDocumentLimitBytes) { + return new SendDocumentResult { + Success = false, + ChatId = toolContext.ChatId, + Error = $"Generated image file is too large for Telegram document upload ({imageBytes.LongLength / 1024 / 1024}MB). Maximum allowed size is {( Env.IsLocalAPI ? "2GB" : "50MB" )}." + }; + } + + var document = InputFile.FromStream(new MemoryStream(imageBytes), fileName); + var replyParameters = GetReplyParameters(replyToMessageId, toolContext); + + using var cts = new CancellationTokenSource(SendTimeout); + var message = await _sendMessage.AddTaskWithResult(async () => await _botClient.SendDocument( + chatId: toolContext.ChatId, + document: document, + replyParameters: replyParameters, + cancellationToken: cts.Token + ), toolContext.ChatId); + + return new SendDocumentResult { + Success = true, + MessageId = message.MessageId, + ChatId = message.Chat.Id + }; + } catch (Exception ex) { + return new SendDocumentResult { + Success = false, + ChatId = toolContext?.ChatId ?? 0, + Error = $"Failed to send original generated image: {ex.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;