Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace ClawdDotNet.Tools.Telegram;
|
||||
|
||||
public static class BuildInfo
|
||||
{
|
||||
public const int Build = 1;
|
||||
public const string Changes = "Polling via IToolJobProvider, Markdown-Support";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ClawdDotNet.Tools.Telegram</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Telegram.Bot" Version="22.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\ClawdDotNet.Core\\ClawdDotNet.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.State;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace ClawdDotNet.Tools.Telegram;
|
||||
|
||||
public sealed class TelegramTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
public string Name => "Telegram";
|
||||
|
||||
public string Description => "Erlaubt das Senden und Empfangen von Nachrichten über Telegram.";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["send_message", "get_updates"],
|
||||
"description": "Die auszuführende Aktion"
|
||||
},
|
||||
"chatId": {
|
||||
"type": "string",
|
||||
"description": "Die ID des Chats (optional, falls Standard-ID konfiguriert)"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Der zu sendende Text (nur für send_message)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximale Anzahl an Nachrichten zum Abrufen (nur für get_updates)"
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
""").RootElement.Clone();
|
||||
|
||||
// ─── IAgentTool ───
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input,
|
||||
AgentToolContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var action = input.GetProperty("action").GetString()
|
||||
?? throw new ArgumentException("'action' is required");
|
||||
|
||||
var botToken = context.ToolConfig.TryGetValue("botToken", out var bt) ? bt?.ToString() : null;
|
||||
if (string.IsNullOrWhiteSpace(botToken))
|
||||
{
|
||||
return ToolResult.Fail("Konfigurationsfehler: 'botToken' ist nicht definiert.");
|
||||
}
|
||||
|
||||
var botClient = new TelegramBotClient(botToken);
|
||||
|
||||
try
|
||||
{
|
||||
return action switch
|
||||
{
|
||||
"send_message" => await HandleSendMessageAsync(botClient, input, context, ct),
|
||||
"get_updates" => await HandleGetUpdatesAsync(botClient, input, context, ct),
|
||||
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Fehler bei Telegram Aktion {Action}", action);
|
||||
return ToolResult.Fail($"Fehler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── IToolJobProvider ───
|
||||
|
||||
public IReadOnlyList<ToolJobDefinition> GetJobDefinitions() =>
|
||||
[
|
||||
new("telegram_poll", "Telegram Polling",
|
||||
"Prüft regelmäßig auf neue Telegram-Nachrichten und weckt den Agenten bei neuen Nachrichten.")
|
||||
];
|
||||
|
||||
public async Task<ToolJobResult> ExecuteJobAsync(
|
||||
string jobTypeId,
|
||||
IReadOnlyDictionary<string, object?> toolConfig,
|
||||
IStateStore stateStore,
|
||||
ILogger logger,
|
||||
CancellationToken ct,
|
||||
string? agentId = null,
|
||||
string? workspacePath = null)
|
||||
{
|
||||
if (jobTypeId != "telegram_poll")
|
||||
return ToolJobResult.NoAction($"Unbekannter Job-Typ: {jobTypeId}");
|
||||
|
||||
var botToken = toolConfig.TryGetValue("botToken", out var bt) ? bt?.ToString() : null;
|
||||
if (string.IsNullOrWhiteSpace(botToken))
|
||||
return ToolJobResult.NoAction("Kein botToken konfiguriert");
|
||||
|
||||
var client = new TelegramBotClient(botToken);
|
||||
|
||||
var offsetKey = "telegram_poll_offset";
|
||||
var lastOffsetStr = await stateStore.GetAsync(offsetKey, ct);
|
||||
var lastOffset = int.TryParse(lastOffsetStr, out var o) ? o : 0;
|
||||
|
||||
try
|
||||
{
|
||||
var updates = await client.GetUpdates(
|
||||
offset: lastOffset, limit: 100, cancellationToken: ct);
|
||||
|
||||
var messages = updates
|
||||
.Where(u => u.Message is not null)
|
||||
.Where(u => IsChatIdAllowed(u.Message!.Chat.Id.ToString(), toolConfig))
|
||||
.ToList();
|
||||
|
||||
if (messages.Count == 0)
|
||||
{
|
||||
if (updates.Length > 0)
|
||||
{
|
||||
var newOffset = updates.Max(u => u.Id) + 1;
|
||||
await stateStore.SetAsync(offsetKey, newOffset.ToString(), ct);
|
||||
}
|
||||
return ToolJobResult.NoAction("Polled: 0 neue Nachrichten");
|
||||
}
|
||||
|
||||
var newOffsetFinal = updates.Max(u => u.Id) + 1;
|
||||
await stateStore.SetAsync(offsetKey, newOffsetFinal.ToString(), ct);
|
||||
|
||||
var summary = string.Join("\n", messages.Select(u =>
|
||||
{
|
||||
var from = u.Message!.From?.Username ?? u.Message.From?.FirstName ?? "Unbekannt";
|
||||
var text = u.Message.Text ?? "(kein Text)";
|
||||
return $"- {from}: {text}";
|
||||
}));
|
||||
|
||||
var wakeMessage = $"[Telegram] {messages.Count} neue Nachricht(en) empfangen:\n\n{summary}";
|
||||
|
||||
logger.LogInformation("Telegram Poll: {Count} neue Nachrichten", messages.Count);
|
||||
|
||||
return ToolJobResult.Wake(wakeMessage, $"Polled: {messages.Count} neue Nachrichten");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Telegram Polling fehlgeschlagen");
|
||||
return ToolJobResult.NoAction($"Polling-Fehler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private Helpers ───
|
||||
|
||||
private async Task<ToolResult> HandleSendMessageAsync(
|
||||
ITelegramBotClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var chatIdStr = input.TryGetProperty("chatId", out var ci) ? ci.GetString() : null;
|
||||
var text = input.TryGetProperty("text", out var t) ? t.GetString() : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(chatIdStr))
|
||||
{
|
||||
chatIdStr = context.ToolConfig.TryGetValue("defaultChatId", out var dci) ? dci?.ToString() : null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(chatIdStr) || string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return ToolResult.Fail("'chatId' (oder Standard-ID in Konfig) und 'text' sind für send_message erforderlich.");
|
||||
}
|
||||
|
||||
if (!IsChatIdAllowed(chatIdStr, context.ToolConfig))
|
||||
{
|
||||
return ToolResult.Fail($"Sicherheitsfehler: Zugriff auf ChatId '{chatIdStr}' ist nicht erlaubt.");
|
||||
}
|
||||
|
||||
await client.SendMessage(new ChatId(chatIdStr), text, cancellationToken: ct);
|
||||
return ToolResult.Ok($"Nachricht erfolgreich an {chatIdStr} gesendet.");
|
||||
}
|
||||
|
||||
private async Task<ToolResult> HandleGetUpdatesAsync(
|
||||
ITelegramBotClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;
|
||||
|
||||
var updates = await client.GetUpdates(limit: limit, cancellationToken: ct);
|
||||
|
||||
var allowedUpdates = updates
|
||||
.Where(u => u.Message != null && IsChatIdAllowed(u.Message.Chat.Id.ToString(), context.ToolConfig))
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
Message = new
|
||||
{
|
||||
u.Message!.Id,
|
||||
ChatId = u.Message.Chat.Id,
|
||||
From = u.Message.From?.Username ?? u.Message.From?.FirstName,
|
||||
u.Message.Text,
|
||||
u.Message.Date
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return ToolResult.Ok(JsonSerializer.Serialize(allowedUpdates, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
private static bool IsChatIdAllowed(string chatId, IReadOnlyDictionary<string, object?> toolConfig)
|
||||
{
|
||||
if (toolConfig.TryGetValue("defaultChatId", out var dci) && dci?.ToString() == chatId)
|
||||
return true;
|
||||
|
||||
if (!toolConfig.TryGetValue("allowedChatIds", out var val) || val is not JsonElement je)
|
||||
return false;
|
||||
|
||||
var allowed = je.EnumerateArray().Select(x => x.GetString()).ToList();
|
||||
return allowed.Contains(chatId);
|
||||
}
|
||||
|
||||
private bool IsChatIdAllowed(string chatId, AgentToolContext context)
|
||||
=> IsChatIdAllowed(chatId, context.ToolConfig);
|
||||
}
|
||||
Reference in New Issue
Block a user