Files
ClawdDotNet/ClawdDotNet_Prompt_TelegramClient.md
T
RichardandClaude Opus 4.8 2fed388c99 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>
2026-07-26 18:21:46 +02:00

24 KiB
Raw Blame History

ClawdDotNet Prompt-Anhang: Tool "TelegramClient"

Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert das Tool TelegramClient, das über die Telegram Client API (MTProto) auf den persönlichen Telegram-Account des Nutzers zugreift. Dieses Tool ist NICHT der bereits vorhandene Telegram Bot — es nutzt die User-API und kann damit auch Nachrichten aus privaten Gruppen lesen, in denen der Nutzer Mitglied ist.

Nur Lese-Zugriff. Kein Senden von Nachrichten.


Voraussetzungen

Telegram API Credentials

Der Nutzer muss einmalig auf https://my.telegram.org/apps eine App registrieren. Ergebnis: api_id (Integer) und api_hash (String). Diese Werte repräsentieren die Anwendung (nicht den User) und werden in der InstanceConfig gespeichert.

Erstmalige Authentifizierung

Beim allerersten Start muss der Nutzer sich interaktiv authentifizieren:

  1. Telefonnummer eingeben
  2. Verifizierungscode eingeben (kommt per Telegram-App, SMS oder Anruf)
  3. Optional: 2FA-Passwort eingeben

Danach wird eine Session-Datei gespeichert. Alle weiteren Starts verwenden diese Session automatisch — kein erneuter Login nötig.

NuGet

<PackageReference Include="WTelegramClient" Version="4.*" />

Architektur: Shared Client, Read-Only Access

Warum ein Shared Client?

Die Telegram Client API erlaubt pro Telefonnummer nur EINE aktive MTProto-Verbindung. Mehrere Agent-Runs dürfen NICHT jeweils einen eigenen WTelegram.Client instanziieren — das würde die Session invalidieren und den Login auf dem echten Telegram-Client killen.

Lösung: Ein einziger WTelegram.Client wird im Host instanziiert und als Singleton an alle Agenten weitergegeben. Das Tool selbst ist stateless und greift über den Shared Client auf Telegram zu.

Host (Program.cs)
  └─ TelegramClientManager (Singleton)
       └─ WTelegram.Client (eine Instanz pro Prozess)
            ├─ Agent A: TelegramClient-Tool → liest Gruppe "Aktien-Chat"
            ├─ Agent B: TelegramClient-Tool → liest Gruppe "Krypto-Signals"
            └─ Agent C: TelegramClient-Tool → liest DMs

Concurrency

WTelegram.Client ist NICHT thread-safe für gleichzeitige API-Calls. Der TelegramClientManager muss alle Aufrufe über einen SemaphoreSlim(1,1) serialisieren. Da wir nur lesen und die Calls schnell sind (<500ms), ist die Serialisierung kein Bottleneck.


TelegramClientManager

Datei: Host/Services/TelegramClientManager.cs

Verwaltet die einzige WTelegram.Client-Instanz. Wird im Host als Singleton registriert.

namespace ClawdDotNet.Host.Services;

using WTelegram;
using TL;

public sealed class TelegramClientManager : IAsyncDisposable
{
    private Client? _client;
    private User?   _self;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly ILogger<TelegramClientManager> _logger;

    // Config-Werte aus InstanceConfig
    private readonly int    _apiId;
    private readonly string _apiHash;
    private readonly string _phoneNumber;
    private readonly string _sessionPath;
    private readonly string? _2faPassword;

    // Event für interaktive Login-Aufforderung (Code-Eingabe via UI)
    public event Func<string, Task<string>>? OnLoginCodeRequired;
    public event Func<Task<string>>?         On2FAPasswordRequired;

    public bool IsConnected => _client?.User != null;
    public User? Self       => _self;

    public TelegramClientManager(
        Config.InstanceConfig config,
        ILogger<TelegramClientManager> logger)
    {
        _logger = logger;

        var tgConfig = config.TelegramClient
            ?? throw new InvalidOperationException("TelegramClient config missing in InstanceConfig");

        _apiId       = tgConfig.ApiId;
        _apiHash     = tgConfig.ApiHash;
        _phoneNumber = tgConfig.PhoneNumber;
        _sessionPath = Path.Combine(config.WorkingDirectory, $"telegram_{config.InstanceId}.session");
        _2faPassword = tgConfig.Password2FA;
    }

    public async Task ConnectAsync(CancellationToken ct)
    {
        // WTelegram.Client mit Config-Callback instanziieren
        _client = new Client(ConfigCallback, _sessionPath);

        // Logging an ILogger umleiten
        Helpers.Log = (lvl, msg) =>
            _logger.Log((Microsoft.Extensions.Logging.LogLevel)lvl, "WTelegram: {Message}", msg);

        _self = await _client.LoginUserIfNeeded();
        _logger.LogInformation(
            "Telegram: logged in as {Name} (id {Id})",
            _self.first_name, _self.id);
    }

    private string? ConfigCallback(string what) => what switch
    {
        "api_id"            => _apiId.ToString(),
        "api_hash"          => _apiHash,
        "phone_number"      => _phoneNumber,
        "session_pathname"  => _sessionPath,

        // Interaktiver Code — wird über Event an die UI weitergeleitet
        "verification_code" => OnLoginCodeRequired != null
            ? OnLoginCodeRequired("Bitte Telegram-Verifizierungscode eingeben:").Result
            : throw new InvalidOperationException(
                "Verification code required but no UI handler registered. " +
                "Connect OnLoginCodeRequired to prompt the user."),

        // 2FA-Passwort — aus Config oder interaktiv
        "password" => _2faPassword
            ?? (On2FAPasswordRequired != null
                ? On2FAPasswordRequired().Result
                : throw new InvalidOperationException(
                    "2FA password required but not configured.")),

        _ => null  // Defaults für alles andere
    };

    /// Alle Dialoge (Chats, Gruppen, Kanäle, DMs) auflisten
    public async Task<Messages_Dialogs> GetAllDialogsAsync(CancellationToken ct)
    {
        await _gate.WaitAsync(ct);
        try { return await _client!.Messages_GetAllDialogs(); }
        finally { _gate.Release(); }
    }

    /// Alle Gruppen/Kanäle auflisten (ohne DMs)
    public async Task<Messages_Chats> GetAllChatsAsync(CancellationToken ct)
    {
        await _gate.WaitAsync(ct);
        try { return await _client!.Messages_GetAllChats(); }
        finally { _gate.Release(); }
    }

    /// Nachrichten aus einem Chat/Kanal/Gruppe lesen
    /// peer: Chat-ID oder Username
    /// minId: nur Nachrichten neuer als diese ID (für Delta-Abfragen)
    /// limit: max. Anzahl Nachrichten
    public async Task<Messages_MessagesBase> GetMessagesAsync(
        InputPeer peer, int minId = 0, int limit = 50, CancellationToken ct = default)
    {
        await _gate.WaitAsync(ct);
        try
        {
            return await _client!.Messages_GetHistory(
                peer, offset_id: 0, offset_date: default,
                add_offset: 0, limit: limit, max_id: 0, min_id: minId, hash: 0);
        }
        finally { _gate.Release(); }
    }

    /// Peer über Username oder Chat-ID auflösen
    public async Task<IPeerInfo> ResolveUsernameAsync(string username, CancellationToken ct)
    {
        await _gate.WaitAsync(ct);
        try { return await _client!.Contacts_ResolveUsername(username.TrimStart('@')); }
        finally { _gate.Release(); }
    }

    /// Peer über bekannte Chat-ID auflösen (benötigt vorherigen GetAllChats/Dialogs Aufruf)
    public InputPeer? GetInputPeerFromCache(long chatId)
        => _client!.GetInputPeerID(chatId);

    public async ValueTask DisposeAsync()
    {
        _client?.Dispose();
        _gate.Dispose();
    }
}

TelegramClientTool — das IAgentTool

Datei: ClawdDotNet.Tools.TelegramClient/TelegramClientTool.cs

namespace ClawdDotNet.Tools.TelegramClient;

using ClawdDotNet.Core.Tools;
using ClawdDotNet.Host.Services;    // TelegramClientManager
using TL;
using System.Text.Json;

public sealed class TelegramClientTool : IAgentTool
{
    // Manager wird per DI injiziert (Singleton im Host)
    private readonly TelegramClientManager _tg;

    public TelegramClientTool(TelegramClientManager tg) => _tg = tg;

    public string Name        => "TelegramClient";
    public string Description => """
        Liest Nachrichten aus dem persönlichen Telegram-Account des Nutzers.
        Zugriff auf alle Chats, Gruppen und Kanäle in denen der Nutzer Mitglied ist.
        NUR LESEN  kein Senden, kein Löschen, kein Bearbeiten.
        Aktionen: list_chats, read_messages, read_new
        """;

    public JsonElement InputSchema => JsonDocument.Parse("""
    {
      "type": "object",
      "required": ["action"],
      "properties": {
        "action": {
          "type": "string",
          "enum": ["list_chats", "read_messages", "read_new"],
          "description": "list_chats: alle Chats/Gruppen/Kanäle auflisten. read_messages: letzte N Nachrichten aus einem Chat lesen. read_new: nur neue Nachrichten seit letztem Abruf."
        },
        "chatId": {
          "type": "integer",
          "description": "Chat-ID aus list_chats Ergebnis. Erforderlich für read_messages und read_new."
        },
        "username": {
          "type": "string",
          "description": "Alternativ zu chatId: @username einer Gruppe/Person auflösen."
        },
        "limit": {
          "type": "integer",
          "description": "Max. Anzahl Nachrichten (default: 30, max: 100)"
        }
      }
    }
    """).RootElement;

    public async Task<ToolResult> ExecuteAsync(
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
    {
        // ---- Permission-Check: Welche Chats darf dieser Agent lesen? ----
        var config = ctx.ToolConfig.GetValueOrDefault("TelegramClient")
                     as Dictionary<string, object?> ?? new();

        var allowedChats = config.GetValueOrDefault("allowedChatIds")
                           as List<long>;   // null = alle erlaubt
        var allowedUsernames = config.GetValueOrDefault("allowedUsernames")
                               as List<string>;

        if (!_tg.IsConnected)
            return new ToolResult(false, "",
                "Telegram-Client ist nicht verbunden. Bitte zuerst authentifizieren.");

        var action = input.GetProperty("action").GetString()!;

        return action switch
        {
            "list_chats"    => await ListChatsAsync(allowedChats, ct),
            "read_messages" => await ReadMessagesAsync(input, allowedChats, config, ctx, ct),
            "read_new"      => await ReadNewAsync(input, allowedChats, config, ctx, ct),
            _               => new ToolResult(false, "", $"Unknown action: {action}")
        };
    }

    private async Task<ToolResult> ListChatsAsync(
        List<long>? allowedChats, CancellationToken ct)
    {
        var dialogs = await _tg.GetAllDialogsAsync(ct);

        var chatList = new List<object>();
        foreach (Dialog dialog in dialogs.dialogs)
        {
            var peer = dialogs.UserOrChat(dialog);
            if (peer == null) continue;

            var chatId = dialog.Peer.ID;

            // Filter: nur erlaubte Chats anzeigen (wenn Whitelist definiert)
            if (allowedChats != null && !allowedChats.Contains(chatId))
                continue;

            var info = peer switch
            {
                User user when user.IsActive => new
                {
                    chatId     = chatId,
                    type       = "user",
                    name       = $"{user.first_name} {user.last_name}".Trim(),
                    username   = user.MainUsername,
                    unread     = dialog.UnreadCount,
                    lastMsgId  = dialog.TopMessage
                } as object,

                ChatBase chat when chat.IsActive => new
                {
                    chatId     = chatId,
                    type       = chat is Channel ch
                                   ? (ch.IsGroup ? "supergroup" : "channel")
                                   : "group",
                    name       = chat.Title,
                    username   = (chat as Channel)?.MainUsername,
                    unread     = dialog.UnreadCount,
                    lastMsgId  = dialog.TopMessage
                } as object,

                _ => null
            };

            if (info != null) chatList.Add(info);
        }

        var result = new
        {
            fetchedAt = DateTime.UtcNow,
            dataAsOf  = DateTime.UtcNow,
            source    = "telegram_client_api",
            data      = new
            {
                totalChats = chatList.Count,
                chats      = chatList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<ToolResult> ReadMessagesAsync(
        JsonElement input, List<long>? allowedChats,
        Dictionary<string, object?> config,
        AgentToolContext ctx, CancellationToken ct)
    {
        var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
        if (error != null) return new ToolResult(false, "", error);

        var limit = input.TryGetProperty("limit", out var l)
                    ? Math.Clamp(l.GetInt32(), 1, 100)
                    : 30;

        var messages = await _tg.GetMessagesAsync(peer!, minId: 0, limit: limit, ct: ct);

        var msgList = FormatMessages(messages);

        var result = new
        {
            fetchedAt = DateTime.UtcNow,
            dataAsOf  = DateTime.UtcNow,
            source    = $"telegram_chat_{chatId}",
            data      = new
            {
                chatId     = chatId,
                count      = msgList.Count,
                messages   = msgList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<ToolResult> ReadNewAsync(
        JsonElement input, List<long>? allowedChats,
        Dictionary<string, object?> config,
        AgentToolContext ctx, CancellationToken ct)
    {
        var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
        if (error != null) return new ToolResult(false, "", error);

        // Letzte bekannte Message-ID aus StateStore laden
        var stateKey = $"tgclient:{ctx.AgentId}:chat_{chatId}:lastMsgId";
        var lastIdStr = await ctx.StateStore.GetAsync(stateKey, ct);
        var lastId = int.TryParse(lastIdStr, out var id) ? id : 0;

        var limit = input.TryGetProperty("limit", out var l)
                    ? Math.Clamp(l.GetInt32(), 1, 100)
                    : 50;

        // min_id = lastId → nur Nachrichten neuer als lastId
        var messages = await _tg.GetMessagesAsync(peer!, minId: lastId, limit: limit, ct: ct);

        var msgList = FormatMessages(messages);

        // Neue Max-ID persistieren
        if (msgList.Count > 0)
        {
            var newMaxId = msgList.Max(m => m.messageId);
            await ctx.StateStore.SetAsync(stateKey, newMaxId.ToString(), ct);
        }

        var result = new
        {
            fetchedAt    = DateTime.UtcNow,
            dataAsOf     = DateTime.UtcNow,
            source       = $"telegram_chat_{chatId}",
            data         = new
            {
                chatId       = chatId,
                sinceId      = lastId,
                newCount     = msgList.Count,
                messages     = msgList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<(InputPeer? peer, long chatId, string? error)> ResolvePeerAsync(
        JsonElement input, List<long>? allowedChats, CancellationToken ct)
    {
        long chatId = 0;
        InputPeer? peer = null;

        if (input.TryGetProperty("chatId", out var cid))
        {
            chatId = cid.GetInt64();
            if (allowedChats != null && !allowedChats.Contains(chatId))
                return (null, chatId,
                    $"Agent hat keinen Zugriff auf Chat {chatId}.");

            peer = _tg.GetInputPeerFromCache(chatId);
            if (peer == null)
            {
                // Cache befüllen durch einmaligen GetAllDialogs-Aufruf
                await _tg.GetAllDialogsAsync(ct);
                peer = _tg.GetInputPeerFromCache(chatId);
            }
        }
        else if (input.TryGetProperty("username", out var uname))
        {
            var resolved = await _tg.ResolveUsernameAsync(uname.GetString()!, ct);
            peer   = resolved?.ToInputPeer();
            chatId = peer?.ID ?? 0;

            if (allowedChats != null && !allowedChats.Contains(chatId))
                return (null, chatId,
                    $"Agent hat keinen Zugriff auf Chat @{uname.GetString()}.");
        }

        if (peer == null)
            return (null, 0, "chatId oder username muss angegeben werden.");

        return (peer, chatId, null);
    }

    private static List<FormattedMessage> FormatMessages(Messages_MessagesBase messages)
    {
        var result = new List<FormattedMessage>();

        foreach (var msgBase in messages.Messages)
        {
            var from = messages.UserOrChat(msgBase.From ?? msgBase.Peer);
            var fromName = from switch
            {
                User u    => $"{u.first_name} {u.last_name}".Trim(),
                ChatBase c => c.Title,
                _         => "Unknown"
            };

            if (msgBase is Message msg)
            {
                result.Add(new FormattedMessage(
                    messageId:  msg.ID,
                    date:       msg.Date,
                    from:       fromName,
                    fromId:     msgBase.From?.ID ?? 0,
                    text:       msg.message,
                    hasMedia:   msg.media != null,
                    mediaType:  msg.media?.GetType().Name,
                    replyToId:  (msg.reply_to as MessageReplyHeader)?.reply_to_msg_id,
                    forwardFrom: msg.fwd_from != null
                        ? msg.fwd_from.from_name ?? "forwarded"
                        : null,
                    views:      msg.views
                ));
            }
        }

        return result.OrderBy(m => m.messageId).ToList();
    }

    private sealed record FormattedMessage(
        int       messageId,
        DateTime  date,
        string    from,
        long      fromId,
        string?   text,
        bool      hasMedia,
        string?   mediaType,
        int?      replyToId,
        string?   forwardFrom,
        int?      views
    );
}

AgentConfig-Beispiel

{
  "agentId": "telegram-scout",
  "displayName": "Telegram News-Scout",
  "model": "google/gemini-flash-1.5",
  "systemPrompt": "Du überwachst Telegram-Gruppen auf relevante Finanznachrichten und Trading-Signale. Fasse neue Nachrichten zusammen und bewerte ihre Relevanz. Verwende niemals Daten ohne fetchedAt-Feld.",
  "tools": {
    "TelegramClient": {
      "allowedChatIds": [1001234567890, 1009876543210],
      "allowedUsernames": ["aktien_chat", "crypto_signals_de"]
    },
    "Database": {
      "connectionString": "...",
      "allowedTables": ["telegram_messages", "signal_archive"]
    }
  },
  "scheduler": {
    "cron": "*/15 * * * *",
    "runOnStart": true
  },
  "loopGuard": {
    "maxSteps": 10,
    "maxTokens": 30000
  }
}

Ein Agent ohne TelegramClient-Eintrag in seiner Config bekommt das Tool gar nicht erst in seinem LLM-Tool-Set angezeigt (normales Permission-Verhalten). Ein Agent MIT Config aber ohne allowedChatIds (= null) darf alle Chats lesen.


InstanceConfig-Erweiterung

// Config/InstanceConfig.cs — neues optionales Feld:

public sealed class InstanceConfig
{
    // ... bestehende Felder ...

    public TelegramClientConfig? TelegramClient { get; set; }
}

public sealed class TelegramClientConfig
{
    public int    ApiId        { get; set; }           // von https://my.telegram.org/apps
    public string ApiHash      { get; set; } = "";     // von https://my.telegram.org/apps
    public string PhoneNumber  { get; set; } = "";     // z.B. "+491701234567"
    public string? Password2FA { get; set; }           // optional, nur bei aktivierter 2FA
}
// In stock-team.json:
{
  "instanceId": "stock-01",
  "instanceName": "Aktien-Team",
  "openRouterApiKey": "sk-or-...",
  "workingDirectory": "./data/stock/",
  "webServerPort": 8081,
  "telegramClient": {
    "apiId": 12345678,
    "apiHash": "abcdef1234567890abcdef1234567890",
    "phoneNumber": "+491701234567"
  },
  "agents": [ ... ]
}

Interaktiver Login in WinForms

Der erste Login erfordert einen Verifizierungscode. Dieser wird über das bestehende Chat-UI in frm_main abgefragt — nicht über die Konsole.

In frm_main oder Program.cs beim Start:

var tgManager = provider.GetRequiredService<TelegramClientManager>();

// UI-Handler für Code-Eingabe registrieren
tgManager.OnLoginCodeRequired = async (prompt) =>
{
    // Auf UI-Thread: InputBox oder Chat-Nachricht anzeigen
    string? code = null;
    mainForm.Invoke(() =>
    {
        code = Microsoft.VisualBasic.Interaction.InputBox(
            prompt, "Telegram Verifizierung", "");
    });
    return code ?? "";
};

tgManager.On2FAPasswordRequired = async () =>
{
    string? pw = null;
    mainForm.Invoke(() =>
    {
        pw = Microsoft.VisualBasic.Interaction.InputBox(
            "Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
    });
    return pw ?? "";
};

// Verbindung herstellen (nutzt Session-Datei wenn vorhanden)
try
{
    await tgManager.ConnectAsync(CancellationToken.None);
}
catch (Exception ex)
{
    _logger.LogError(ex, "Telegram: Login fehlgeschlagen");
    // App startet trotzdem — TelegramClient-Tool meldet "nicht verbunden"
}

Nach erfolgreichem Login wird die Session-Datei ./data/{instanceId}/telegram_{instanceId}.session gespeichert. Alle weiteren Starts loggen automatisch ein — kein Code mehr nötig.


Sicherheitsregeln

  1. NUR LESEN — Das Tool implementiert keine Sende-Funktionen. Es gibt keine send_message-Action. Der TelegramClientManager exponiert bewusst keine SendMessageAsync-Methode.

  2. Session-Datei ist sensibel — Sie enthält die Auth-Keys für den Telegram-Account. Die Datei liegt im WorkingDirectory und darf NICHT vom FileRW-Tool erreichbar sein. In der AgentConfig für FileRW darf der rootPath NIEMALS auf das WorkingDirectory zeigen wenn dort die Session-Datei liegt. Empfehlung: Session-Datei in einem Unterordner ./data/{instanceId}/sessions/ speichern, der für kein FileRW-Tool als rootPath konfiguriert ist.

  3. Chat-Whitelist pro Agent — Über allowedChatIds kann eingeschränkt werden welche Chats ein Agent lesen darf. Ein Finanzmarkt-Agent hat keinen Zugriff auf private DMs. Ein SEO-Agent hat keinen Zugriff auf Trading-Gruppen.

  4. Rate Limiting — Die Telegram Client API hat undokumentierte Rate-Limits. Bei zu vielen Requests kommt ein FLOOD_WAIT_X Error. Der TelegramClientManager muss FloodException abfangen und await Task.Delay(ex.X * 1000) warten bevor er den Call wiederholt. Empfehlung: mindestens 1 Sekunde Pause zwischen aufeinanderfolgenden API-Calls (der SemaphoreSlim allein reicht nicht).


Besonderheiten von WTelegramClient

Terminology-Mapping

In der Telegram Client API unterscheiden sich die Begriffe von der Benutzeroberfläche:

Telegram-App API-Bezeichnung C#-Typ
Gruppe (klein) Chat Chat
Gruppe (groß) Channel mit IsGroup Channel (IsGroup)
Kanal Channel ohne IsGroup Channel (!IsGroup)
Privatnachricht User User

access_hash-Problem

Telegram-API-Calls benötigen für die meisten Peers einen access_hash. Dieser wird automatisch gecacht wenn vorher Messages_GetAllDialogs() oder Messages_GetAllChats() aufgerufen wurde. Deshalb MUSS bei jedem Start (nach Login) einmalig GetAllDialogsAsync() aufgerufen werden, bevor GetMessagesAsync() funktioniert.

Session-Datei

  • Pfad konfigurierbar über session_pathname in der Config-Callback
  • Verschlüsselt (Standard-Verschlüsselung von WTelegramClient)
  • NICHT zwischen Rechnern portierbar (an Hardware gebunden)
  • Bei Session-Problemen: Datei löschen → neuer Login erforderlich

Implementierungsreihenfolge (für Claude Code)

  1. TelegramClientConfig zu InstanceConfig hinzufügen
  2. TelegramClientManager implementieren (Singleton, SemaphoreSlim, Rate-Limit-Schutz)
  3. TelegramClientTool implementieren (list_chats, read_messages, read_new)
  4. Host: Login-Flow in Program.cs / frm_main integrieren (InputBox für Code)
  5. Sicherheits-Check: Session-Pfad darf nicht in FileRW-rootPath liegen
  6. xUnit-Tests: FormatMessages-Serialisierung, Chat-Whitelist-Filter, Rate-Limit-Handling
  7. Beispiel-Config ergänzen: stock-team.json mit TelegramClient-Eintrag

Beginne mit Schritt 1 dieses Abschnitts.