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,718 @@
|
||||
# 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
|
||||
|
||||
```xml
|
||||
<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.
|
||||
|
||||
```csharp
|
||||
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`**
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// 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:**
|
||||
|
||||
```csharp
|
||||
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.**
|
||||
Reference in New Issue
Block a user