CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
channel-plugins.md211 linesDownload Raw Back to developers
1# Channel Plugin Developer Guide2 3A channel plugin connects Qwen Code to a messaging platform. It's packaged as an [extension](../users/extension/introduction) and loaded at startup. For user-facing docs on installing and configuring plugins, see [Plugins](../users/features/channels/plugins).4 5## How It Fits Together6 7Your plugin sits in the Platform Adapter layer. You handle platform-specific concerns (connecting, receiving messages, sending responses). `ChannelBase` handles everything else (access control, session routing, prompt queuing, slash commands, crash recovery).8 9```10Your Plugin  →  builds Envelope  →  handleInbound()11ChannelBase  →  gates → commands → routing → ChannelAgentBridge.prompt()12ChannelBase  →  calls your sendMessage() with the agent's response13```14 15`ChannelAgentBridge` is the adapter-facing bridge contract. The current standalone `qwen channel start` path provides an `AcpBridge`, but plugin code should type constructor parameters as `ChannelAgentBridge` so the same adapter can run behind other bridge implementations later.16 17Migration note for existing TypeScript plugins: if your adapter constructor or factory explicitly types `bridge` as `AcpBridge`, change that annotation to `ChannelAgentBridge` and keep using only the methods exposed by that contract. JavaScript plugins are unaffected at runtime, and standalone `qwen channel start` still passes the current `AcpBridge` implementation.18 19## Runtime Modes20 21The same plugin adapter can be hosted by either channel runtime:22 23- `qwen channel start [name]` is the standalone ACP-backed service. It still uses `AcpBridge` and remains the stable command for running channels outside a daemon.24- `qwen serve --channel <name>` and repeatable `--channel` flags start an experimental daemon-managed channel worker. `--channel all` starts all configured channels. The worker is owned by `qwen serve`, connects to that daemon through the SDK, and passes adapters a `ChannelAgentBridge` facade backed by `DaemonChannelBridge`.25 26Daemon-managed channels inherit the daemon's lifecycle and status reporting. They are intentionally out-of-process so adapter or platform SDK failures do not crash the daemon. The daemon is still bound to one workspace, so every selected channel config must use a `cwd` that resolves to the daemon workspace.27 28## The Plugin Object29 30Your extension entry point exports a `plugin` conforming to `ChannelPlugin`:31 32```typescript33import type { ChannelPlugin } from '@qwen-code/channel-base';34import { MyChannel } from './MyChannel.js';35 36export const plugin: ChannelPlugin = {37  channelType: 'my-platform', // Unique ID, used in settings.json "type" field38  displayName: 'My Platform', // Shown in CLI output39  requiredConfigFields: ['apiKey'], // Validated at startup (beyond standard ChannelConfig)40  createChannel: (name, config, bridge, options) =>41    new MyChannel(name, config, bridge, options),42};43```44 45## The Channel Adapter46 47Extend `ChannelBase` and implement three methods:48 49```typescript50import { ChannelBase } from '@qwen-code/channel-base';51import type {52  ChannelBaseOptions,53  ChannelAgentBridge,54  ChannelConfig,55  Envelope,56} from '@qwen-code/channel-base';57 58export class MyChannel extends ChannelBase {59  constructor(60    name: string,61    config: ChannelConfig,62    bridge: ChannelAgentBridge,63    options?: ChannelBaseOptions,64  ) {65    super(name, config, bridge, options);66  }67 68  async connect(): Promise<void> {69    // Connect to your platform, register message handlers70    // When a message arrives:71    const envelope: Envelope = {72      channelName: this.name,73      senderId: '...', // Stable, unique platform user ID74      senderName: '...', // Display name75      chatId: '...', // Chat/conversation ID (distinct for DMs vs groups)76      text: '...', // Message text (strip @mentions)77      isGroup: false, // Accurate — used by GroupGate78      isMentioned: false, // Accurate — used by GroupGate79      isReplyToBot: false, // Accurate — used by GroupGate80    };81    this.handleInbound(envelope);82  }83 84  async sendMessage(chatId: string, text: string): Promise<void> {85    // Format markdown → platform format, chunk if needed, deliver86  }87 88  disconnect(): void {89    // Clean up connections90  }91}92```93 94Most adapters should pass `options` through unchanged. If an adapter creates its own `SessionRouter` and passes that router to `super()`, set `registerBridgeEvents: true` in `ChannelBaseOptions` so `ChannelBase` still receives `toolCall` and `sessionDied` events directly. Leave it unset for routers supplied by the channel gateway.95 96If your adapter exposes shell-command behavior, check that `bridge.shellCommand` exists before enabling it. Daemon-managed workers omit that optional method unless the daemon advertises the `session_shell_command` capability.97 98## The Envelope99 100The normalized message object you build from platform data. The boolean flags drive gate logic, so they must be accurate.101 102| Field            | Type         | Required | Notes                                                                      |103| ---------------- | ------------ | -------- | -------------------------------------------------------------------------- |104| `channelName`    | string       | Yes      | Use `this.name`                                                            |105| `senderId`       | string       | Yes      | Must be stable across messages (used for session routing + access control) |106| `senderName`     | string       | Yes      | Display name                                                               |107| `chatId`         | string       | Yes      | Must distinguish DMs from groups                                           |108| `text`           | string       | Yes      | Strip bot @mentions                                                        |109| `threadId`       | string       | No       | For `sessionScope: "thread"`                                               |110| `messageId`      | string       | No       | Platform message ID — useful for response correlation                      |111| `isGroup`        | boolean      | Yes      | GroupGate relies on this                                                   |112| `isMentioned`    | boolean      | Yes      | GroupGate relies on this                                                   |113| `isReplyToBot`   | boolean      | Yes      | GroupGate relies on this                                                   |114| `referencedText` | string       | No       | Quoted message — prepended as context                                      |115| `imageBase64`    | string       | No       | Base64-encoded image (legacy — prefer `attachments`)                       |116| `imageMimeType`  | string       | No       | e.g., `image/jpeg` (legacy — prefer `attachments`)                         |117| `attachments`    | Attachment[] | No       | Structured media attachments (see below)                                   |118 119### Attachments120 121Use the `attachments` array for images, files, audio, and video. `handleInbound()` resolves them automatically: images with base64 `data` are sent to the model as vision input, files with a `filePath` get their path appended to the prompt so the agent can read them.122 123```typescript124interface Attachment {125  type: 'image' | 'file' | 'audio' | 'video';126  data?: string; // base64-encoded data (images, small files)127  filePath?: string; // absolute path to local file (large files saved to disk)128  mimeType: string; // e.g. 'application/pdf', 'image/jpeg'129  fileName?: string; // original file name from the platform130}131```132 133Example — handling a file upload in your adapter:134 135```typescript136import { writeFileSync, mkdirSync, existsSync } from 'node:fs';137import { join } from 'node:path';138import { tmpdir } from 'node:os';139 140const buf = await downloadFromPlatform(fileId);141const dir = join(tmpdir(), 'channel-files');142if (!existsSync(dir)) mkdirSync(dir, { recursive: true });143const filePath = join(dir, fileName);144writeFileSync(filePath, buf);145 146envelope.attachments = [147  {148    type: 'file',149    filePath,150    mimeType: 'application/pdf',151    fileName,152  },153];154```155 156The legacy `imageBase64`/`imageMimeType` fields still work for backwards compatibility but `attachments` is preferred for new code.157 158## Extension Manifest159 160Your `qwen-extension.json` declares the channel type. The key must match `channelType` in your plugin object:161 162```json163{164  "name": "my-channel-extension",165  "version": "1.0.0",166  "channels": {167    "my-platform": {168      "entry": "dist/index.js",169      "displayName": "My Platform Channel"170    }171  }172}173```174 175## Optional Extension Points176 177**Custom slash commands** — register in your constructor:178 179```typescript180this.registerCommand('mycommand', async (envelope, args) => {181  await this.sendMessage(envelope.chatId, 'Response');182  return true; // handled, don't forward to agent183});184```185 186**Working indicators** — override `onPromptStart()` and `onPromptEnd()` to show platform-specific typing indicators. These hooks fire only when a prompt actually begins processing — not for buffered messages (collect mode) or gated/blocked messages:187 188```typescript189protected override onPromptStart(chatId: string, sessionId: string, messageId?: string): void {190  this.platformClient.sendTyping(chatId); // your platform API191}192 193protected override onPromptEnd(chatId: string, sessionId: string, messageId?: string): void {194  this.platformClient.stopTyping(chatId);195}196```197 198**Tool call hooks** — override `onToolCall()` to display agent activity (e.g., "Running shell command...").199 200**Streaming hooks** — override `onResponseChunk(chatId, chunk, sessionId)` for per-chunk progressive display (e.g., editing a message in-place). Override `onResponseComplete(chatId, fullText, sessionId)` to customize final delivery.201 202**Block streaming** — set `blockStreaming: "on"` in the channel config. The base class automatically splits responses into multiple messages at paragraph boundaries. No plugin code needed — it works alongside `onResponseChunk`.203 204**Media** — populate `envelope.attachments` with images/files. See [Attachments](#attachments) above.205 206## Reference Implementations207 208- **Plugin example** (`packages/channels/plugin-example/`) — minimal WebSocket-based adapter, good starting point209- **Telegram** (`packages/channels/telegram/`) — full-featured: images, files, formatting, typing indicators210- **DingTalk** (`packages/channels/dingtalk/`) — stream-based with rich text handling211 
basant307/AI_Governance_Project · CoolFace