Skip to Content
EntwicklerhandbuchDaemonChannel-Adapter

Channel-Adapter

Übersicht

packages/channels/ enthält die IM-Channel-Adapter, die eine eingehende Nachricht einer Chat-Plattform in einen Agent-Prompt umwandeln und die Agent-Antwort zurück an die Chat-Plattform senden. Aktuell sind vier konkrete Channels verfügbar: DingTalk, WeChat (Weixin), Telegram und Feishu. Sie teilen sich eine Basisschicht (packages/channels/base/) und einen adapterseitigen ChannelAgentBridge-Contract.

Es gibt zwei Host-Modi:

  • qwen channel start [name] ist der eigenständige, ACP-gestützte Channel-Service. Er übergibt den Adaptern eine AcpBridge-Implementierung von ChannelAgentBridge.
  • qwen serve --channel <name> und qwen serve --channel all sind experimentelle, vom Daemon verwaltete Modi. Benannte Auswahlen werden nach besitzendem Workspace gruppiert und qwen serve startet einen Out-of-Process-Worker pro besitzender Runtime; jeder Worker verbindet sich über das SDK mit dem Daemon und die Adapter erhalten eine von DaemonChannelBridge gestützte ChannelAgentBridge-Fassade. --channel all bleibt eine Primary-only-Auswahl.

Im Daemon-verwalteten Modus mappt jeder Channel den eingehenden Chat-Traffic auf Daemon-Sessions unter einem konfigurierbaren SessionScope (user, chat_thread oder single). Der Legacy-Channel-Wert thread bleibt für bestehende Konfigurationen les- und schreibbar, aber neue WebShell-Konfigurationen bieten ihn nicht an; dies ist getrennt vom single/thread-Session-Creation-Knob der Daemon-Bridge. Der Adapter delegiert an DaemonChannelBridge, was wiederum an den DaemonSessionClient des SDKs delegiert (siehe 13-sdk-daemon-client.md). Jeder benannte Channel muss einem registrierten, vertrauenswürdigen Workspace aufgelöst werden. Der Worker verwendet das kanonische cwd, QWEN_DAEMON_WORKSPACE und das Environment-Overlay dieser Runtime; die Ownership-Auflösung fällt niemals auf Primary zurück.

Webhook-getriggerte Channel-Tasks

Webhook-getriggerte Tasks werden von qwen serve gehostet und innerhalb des Daemon-verwalteten Channel-Workers ausgeführt. Die HTTP-Route validiert die Quelle und leitet einen ChannelWebhookTask über IPC an den Worker weiter. Der Worker ruft ChannelBase.runWebhookTask() auf, sodass Adapter keine Webhook-Parsung implementieren.

Adapter beteiligen sich weiterhin über Proactive-Send-Support: supportsProactiveSend() teilt dem Host mit, ob ein Channel ohne eingehende Nachricht senden kann, supportsProactiveTarget() behandelt Zustelllimits für bestimmte Target-Formen und pushProactive() transportiert den ausgehenden Inhalt.

Aufgaben

  • Empfangen eingehender Nachrichten vom nativen Transport des Channels (DingTalk WebSocket-Stream, WeChat HTTP Long-Poll, Telegram Bot Long-Poll, Feishu WebSocket oder HTTP Webhook).
  • Auflösen von (senderId, groupId?) in eine Daemon-Session über DaemonChannelSessionFactory.
  • Weiterleiten der Benutzer-Nachricht als Daemon-Prompt und Zurückstreamen der Antwort als ausgehende Chat-Nachrichten, ggf. in Chunks.
  • Rendern von Berechtigungsanfragen als chat-native Prompts, wenn interaktiv; andernfalls automatisches Genehmigen gemäß ChannelConfig.approvalMode.
  • Anwenden von Sender-Gating (Allowlists / Denylists), Group-Gating und Inhaltsnormalisierung (Markdown / HTML je nach Channel).

Architektur

DaemonChannelBridge (gemeinsame Basis, packages/channels/base/src/DaemonChannelBridge.ts)

class DaemonChannelBridge extends EventEmitter { constructor(opts: { cwd: string; sessionFactory: DaemonChannelSessionFactory; modelServiceId?: string; sessionScope?: SessionScope; }); newSession(cwd: string): Promise<string>; loadSession(sessionId: string, cwd: string): Promise<string>; prompt(sessionId: string, text: string, options?): Promise<string>; cancelSession(sessionId: string): Promise<void>; stop(): void; }

Hält Daemon-Session-Clients, gekeyed nach Daemon-sessionId; ChannelBase und SessionRouter entscheiden, welches eingehende Chat-Ziel auf diese Session gemappt wird. Jede angehängte Session verfügt über:

  • Einen DaemonChannelSessionClient (Form von DaemonSessionClient ohne channel-irrelevante Methoden).
  • Eine Live-SSE-Consumer-Pump.
  • Einen Debounced-Prompt-Assembler (für Adapter, die Benutzereingaben über mehrere eingehende Nachrichten fragmentieren).
  • Eine Auto-Approve-Richtlinie pro Anfrage.

Ausgegebene Events: textChunk, toolCall, sessionUpdate, permissionRequest, permissionResolved, modelSwitched, modelSwitchFailed, sessionDied, promptComplete und error. Channel-Adapter verdrahten diese mit plattformspezifischen APIs.

ChannelBase (packages/channels/base/src/ChannelBase.ts)

Abstrakte Basisklasse, die jeder Adapter erweitert:

abstract class ChannelBase { abstract connect(): Promise<void>; abstract sendMessage(chatId: string, text: string): Promise<void>; abstract disconnect(): void; handleInbound(envelope: Envelope): Promise<void>; // → SessionRouter.resolve + bridge.prompt }

Die gesamte interne Nachrichtenzustellung läuft über sendThreadMessage(chatId, threadId, text). Die Standard-Implementierung fällt auf sendMessage(chatId, text) zurück und ignoriert threadId – IM-Adapter sind nicht betroffen. Polling-Adapter (z. B. GitHub) überschreiben sendThreadMessage, um Kommentare zu einem bestimmten Issue/PR über die threadId zu posten.

Behandelt gängige Cross-Cutting-Concerns: Sender-Gating (Allowlist / Denylist), Group-Gating, Message-Block-Streaming (Chunk-Größe, Throttling), Inbound-Debounce.

Channel-spezifische Adapter

AdapterDateiTransportHinweise
DingTalkpackages/channels/dingtalk/src/DingtalkAdapter.tsDingTalk Stream SDK WebSocketSendet via sessionWebhook POST; Medien-Bilder werden über die DT-API heruntergeladen, base64 im Envelope.
WeChat (Weixin)packages/channels/weixin/src/WeixinAdapter.tsiLink Bot HTTP long-pollSendet über proprietäre sendText / sendImage API; Typing-Indikatoren.
Telegrampackages/channels/telegram/src/TelegramAdapter.tsTelegram Bot API long-poll (grammy)Sendet HTML-Chunks via sendMessage.
Feishupackages/channels/feishu/src/FeishuAdapter.tsFeishu/Lark Stream WebSocket (default) oder HTTP webhookSendet über Lark SDK als interaktive Karten; Webhook-Modus erfordert encryptKey für HMAC-Signaturverifizierung.
GitHubpackages/channels/github/src/GithubAdapter.tsGitHub Notifications API polling (@octokit/rest)Erweitert PollingChannelBase; Cursor-basiertes Comment-Window-Dedup; postet Kommentare über die Issues API.
GitLabpackages/channels/gitlab/src/GitlabAdapter.tsGitLab Todos API polling (@gitbeaker/rest)Erweitert PollingChannelBase; dispatcht todo.body direkt; action_prompt_template-Konfiguration steuert Event-Filterung und Metadata-Rendering.

Jeder Adapter implementiert:

  1. Inbound-Transport (Subscriben / Pollen auf Nachrichten).
  2. Envelope-Konstruktion ({ senderId, groupId?, text, media?, raw }).
  3. Sender- / Group-Gating (delegiert an ChannelBase).
  4. Outbound-Serialisierung (Markdown → HTML / WeChat-nativ / DingTalk-nativ).
  5. Lifecycle (Start / Shutdown).

Adapter-Matrix

AdapterTransportIdentitätPermission-UXAuto-Approve-Konfiguration
DingTalkWebSocket streamsenderStaffId (+ optional conversationId für Gruppen)Inline-Buttons via DT-MarkdownChannelConfig.approvalMode = 'auto' | 'prompt'
WeChatHTTP long-pollsenderWxid (+ optional groupWxid)Text-Prompts mit Reply-TokensGleich
TelegramBot API long-pollfrom.id (+ optional chat.id für Gruppen)Inline-Keyboard-ButtonsGleich
FeishuWebSocket stream / HTTP webhooksender.open_id (+ optional chat_id für Gruppen)Interaktive Karten-ButtonsGleich
GitHubNotifications API pollingNumerische user.id (unveränderlich; Login beim Connect aufgelöst)Error-Kommentar + Re-MentionsenderPolicy: 'allowlist' | 'open'
GitLabTodos API pollingauthor.username (lowercased)Log + Re-MentionsenderPolicy: 'allowlist' | 'open'

Hinweis: Die Spalte “Permission-UX” beschreibt die native Affordanz jeder Plattform, aber keine ist derzeit verdrahtet – AcpBridge.requestPermission genehmigt derzeit jede Anfrage automatisch (packages/channels/base/src/AcpBridge.ts), und ChannelConfig.approvalMode ist deklariert, wird aber noch nicht ausgelesen. Interaktives Genehmigen ist geplant (Phase 5).

Workflow

Inbound-Prompt

SSE-gesteuerter Outbound

Permission-Auto-Approve

State & Lifecycle

  • DaemonChannelBridge lebt für die Lebensdauer des Channel-Adapters; Sessions darin leben gemäß dem konfigurierten SessionScope.
  • Jede aktive Session verbindet sich automatisch neu, wenn SSE abbricht – DaemonSessionClient.events() trackt lastSeenEventId, sodass das Replay korrekt ist.
  • shutdown() schließt jede aktive Session und den zugrunde liegenden Transport (WebSocket / Long-Poll des Channels).
  • Der WebSocket-Stream von DingTalk unterstützt Server-Push; der Long-Poll von WeChat erfordert eine Backoff-Strategie bei Idle-Responses; der Long-Poll von Telegram hat einen eingebauten timeout-Parameter.

Runtime-Auswahl und Settings-Reload

Der langlebige ChannelWorkerManager besitzt die feste Daemon-Auswahl und die nach Workspace gruppierten Supervisoren. Ein Daemon kann ohne --channel booten; das erste strikt-gatede PUT /workspace/channel lädt dynamisch die Channel-Runtime, reserviert die Service-PID-Datei, löst die Workspace-Ownership auf und startet die ausgewählten Worker. GET /workspace/channel liest den Manager-Snapshot und DELETE /workspace/channel stoppt ihn idempotent. SDK-Helper sind getChannelWorkerControl(), setChannelWorkerSelection() und stopChannelWorker(); der CLI-Einstieg ist qwen channel set plus die Remote-Varianten status und stop.

Der Daemon liest die Channel-Einstellungen aus settings.json, wenn jeder Worker startet (packages/cli/src/commands/channel/daemon-worker.tsloadSettingsloadChannelsConfig). POST /workspace/channel/reload liest diese Einstellungen neu und erzwingt die Abstimmung mit der festen Auswahl. Alle Lifecycle-Mutationen teilen sich eine FIFO-Lane. Unveränderte Workspace-Gruppen überleben die gewöhnliche Auswahl-Ersetzung; geänderte Gruppen werden sequenziell gestoppt und gestartet, während die Serve-eigene PID-Lease gehalten wird.

Schlägt eine Ersetzung fehl, werden neu gestartete Worker gestoppt und alte Worker wiederhergestellt, bevor die Anfrage zurückkehrt. Ein Supervisor, der nach SIGTERM und SIGKILL keinen Exit beobachten kann, behält seine Kind-Referenz und schlägt beim Stoppen fehl; der Manager behält die PID-Lease und startet niemals einen zweiten Worker. Webhook-Konfiguration und -Routing ändern sich nur, wenn die Auswahl-Übernahme erfolgreich ist. Runtime-Auswahlen sind prozesslokal und verschwinden beim Daemon-Neustart.

Adapter-connect()-Fehler werden separat von Worker-Lifecycle-Fehlern gemeldet. Der Worker sendet jeden begrenzten, um Credentials bereinigten Fehler über die Startup-IPC und wartet auf eine Supervisor-Bestätigung, bevor er den nächsten Adapter versucht. Ein teilweise verbundener Worker bleibt laufend und exponiert startupFailures in seinem Snapshot. Schlägt jeder Adapter in einem dynamischen Versuch fehl, trägt die 502 channel_worker_start_failed-Antwort die mit Workspace annotierten, versuchten Fehler, während state das Rollback-Ergebnis widerspiegelt; nachfolgende GET-Antworten behalten den Versuch nicht. Der Daemon-Boot ohne verbundenen Adapter bleibt fail-fast. Der optionale Adapter-code ist nur diagnostisch und die aktuelle phase ist connect.

Abhängigkeiten

  • packages/channels/base/ChannelBase, PollingChannelBase, DaemonChannelBridge, types.ts (ChannelConfig, Envelope, SessionScope, ChannelPlugin).
  • packages/sdk-typescript/src/daemon/DaemonSessionClient und verwandte Klassen.
  • Channel-spezifische SDKs: @dingtalk/stream (DingTalk), proprietärer iLink Bot HTTP (Weixin), grammy (Telegram), @octokit/rest (GitHub-Polling), @gitbeaker/rest (GitLab-Polling).

Konfiguration

ChannelConfig (aus packages/channels/base/src/types.ts):

KnobEffekt
sessionScope'user' (Sender + Chat), 'chat_thread' (Channel + chatId + threadId) oder 'single' (eine gemeinsame Session pro Channel). Legacy 'thread' bleibt bei bestehender Konfiguration erhalten, wird aber für neue WebShell-Konfigurationen nicht angeboten.
approvalMode'auto' (automatisch antworten) / 'prompt' (UI rendern).
allowlist?: string[]Erlaubte Sender-IDs; fehlend = offen.
denylist?: string[]Abgewiesene Sender-IDs.
chunkSize, chunkIntervalMsOutbound-Block-Streaming-Einstellungen.
daemon: { baseUrl, token?, clientId? }Wird an DaemonChannelSessionFactory weitergeleitet.

Channel-spezifische Keys werden darüber hinaus hinzugefügt (DingTalk: streamCredentials; WeChat: ilinkUrl, botId; Telegram: botToken; Feishu: clientId (appId), clientSecret (appSecret), verificationToken, encryptKey (Webhook-Modus)).

Einschränkungen & bekannte Limits

  • Channels importieren nicht direkt @qwen-code/sdk. Sie gehen über ChannelBaseDaemonChannelBridgeDaemonChannelSessionClient (welches der Bridge aus dem SDK konstruiert). Diese Indirektion ermöglicht es der Bridge, Implementierungen auszutauschen, wie z. B. einen Test-Stub, ohne Änderungen an den Channels zu erfordern.
  • Permission-UX ist channel-spezifisch. DingTalk verwendet Markdown-Buttons; WeChat ist textbasiert; Telegram nutzt Inline-Keyboards; Feishu verwendet interaktive Karten-Buttons. (Alle genehmigen derzeit automatisch über AcpBridge; interaktives Genehmigen ist geplant.) Es gibt noch keine gemeinsame Abstraktion für ein “interaktives Permission-Widget”.
  • Auto-Approve ist eine Entscheidung auf Deployment-Seite, nicht auf Daemon-Seite. Die permission_mediation-Richtlinie des Daemons gilt weiterhin; Auto-Approve bedeutet nur, dass der Channel antwortet, ohne den Menschen zu prompten. Kombiniere auto nicht mit Workflows der enforce-Klasse.
  • Channel-spezifische Rate-Limits / Nachrichtengrößen-Limits sind Aufgabe des Adapters. DaemonChannelBridge übernimmt nur das Chunking; das Überschreiten der Nachrichtengröße von WeChat oder des Flood-Limits von Telegram liegt beim Adapter.
  • Keine DingTalk- / WeChat- / Telegram- / Feishu-Reverse-Calls – Channels sind unidirektional (Chat → Daemon → Chat). Der native Push-Pfad der IM-Plattform, wie z. B. ein DingTalk-Card-Callback, ist noch nicht in die Bridge integriert.

Referenzen

  • packages/channels/base/src/DaemonChannelBridge.ts
  • packages/channels/base/src/ChannelBase.ts
  • packages/channels/base/src/types.ts
  • packages/cli/src/serve/channel-worker-manager.ts (Auswahl-Lifecycle + Serialisierung)
  • packages/cli/src/serve/channel-worker-group.ts (Workspace-Differential-Reconcile)
  • packages/cli/src/serve/channel-worker-supervisor.ts (Kind-Supervision)
  • packages/cli/src/serve/routes/workspace-channel-control.ts (GET/PUT/DELETE/reload-Ressource)
  • packages/channels/dingtalk/src/DingtalkAdapter.ts
  • packages/channels/weixin/src/WeixinAdapter.ts
  • packages/channels/telegram/src/TelegramAdapter.ts
  • packages/channels/plugin-example/ (Referenz-Plugin-Scaffold)
  • Channel-Plugin-Guide: ../channel-plugins.md.
  • SDK-Referenz: 13-sdk-daemon-client.md.
Last updated on