Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Docs/Bot_Commands_User_Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 机器人回复: `音乐工具已开启...`
Expand Down
2 changes: 1 addition & 1 deletion Docs/README_MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 高级功能:
- 短链接映射服务
- 消息扩展存储
Expand Down Expand Up @@ -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服务器(`新建渠道`等)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class ImageGenerationResult {
public string Endpoint { get; set; } = null!;
public List<GeneratedImageInfo> Images { get; set; } = new();
public List<SendPhotoResult> SentPhotos { get; set; } = new();
public List<SendDocumentResult> SentOriginalFiles { get; set; } = new();
}

public class GeneratedImageInfo {
Expand Down
72 changes: 65 additions & 7 deletions TelegramSearchBot/Service/Tools/ImageGenerationToolService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down Expand Up @@ -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<ImageGenerationResult> GenerateImage(
[BuiltInParameter("Image prompt. Be specific about subject, style, composition, lighting, colors, and any text that should appear.")] string prompt,
ToolContext toolContext,
Expand All @@ -274,6 +275,7 @@ public async Task<ImageGenerationResult> 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) {
Expand Down Expand Up @@ -358,18 +360,28 @@ public async Task<ImageGenerationResult> 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;
Expand Down Expand Up @@ -648,7 +660,7 @@ private async Task<GeneratedImagePayload> SaveImageAsync(
});
}

private async Task<SendPhotoResult> SendGeneratedImageAsync(
private async Task<SendPhotoResult> SendGeneratedImagePhotoAsync(
byte[] imageBytes,
string fileName,
ToolContext toolContext,
Expand Down Expand Up @@ -699,6 +711,52 @@ private async Task<SendPhotoResult> SendGeneratedImageAsync(
}
}

private async Task<SendDocumentResult> 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;
Expand Down
Loading