qwen-code/packages/channels/plugin-example/README.md
qqqys 6509e8de08
feat(channels): show lifecycle status in adapters (#6114)
* chore: ignore local worktrees

* docs(channels): design identity and task lifecycle p0

* docs(channels): plan identity and task lifecycle p0

* feat(channels): add identity and task lifecycle metadata

* fix(channels): suppress cancelled tool call lifecycle

* docs: add channel lifecycle status adapter design

* docs: add channel lifecycle status adapter plan

* feat(channels): map telegram lifecycle to typing

* feat(channels): map weixin lifecycle to typing

* fix(channels): reset weixin typing state after failed start

* feat(channels): map dingtalk lifecycle to reactions

* feat(channels): add feishu card status labels

* fix(channels): preserve feishu collapsible status labels

* feat(channels): show feishu lifecycle card status

* fix(channels): cover loop lifecycle metadata

* fix(channels): preserve feishu terminal card status

* chore: remove internal task reports from branch

* fix(channels): clear late lifecycle status starts

* fix(channels): harden lifecycle event edges

* fix(channels): clean adapter lifecycle state

* fix(channels): finalize cancelled lifecycle before cleanup

* fix(feishu): keep cancelled card status visible

* docs(channels): add lifecycle status no-op coverage

* fix(channels): address lifecycle review suggestions

* docs(channels): align lifecycle status documentation

* fix(channels): route adapter stop through lifecycle cancel

* fix(channels): close lifecycle cancellation races

* fix(channels): keep first feishu terminal status

* fix(telegram): guard lifecycle typing updates

* test(qqbot): cover lifecycle status no-ops

* fix(feishu): preserve completed card race status

* docs(lifecycle): align status review docs

* fix(channels): separate pending cancel state

* fix(channels): clean adapter lifecycle status edges

* fix(channels): order clear cancellation lifecycle

* fix(feishu): preserve user stop status label

* fix(channels): suppress loop chunks during pending cancel

* test(channels): use active session in cancel regression

* fix(channels): harden adapter cancellation tests

* fix(channels): sanitize lifecycle tool fields

* fix(channels): route shared tool call lifecycle

* fix(channels): preserve pending cancel intent

* fix(channels): preserve pending cancel intent

* fix(telegram): track typing by session

* fix(feishu): preserve stop status during finalization

* fix(channels): preserve responses after failed cancel

* fix(channels): tighten lifecycle cancellation reasons

* fix(channels): close lifecycle cancel races and validate identity config

Address the outstanding review findings on #6105:

- carry a typed reason on ChannelLoopSkippedError and report disabled
  loops as 'dropped' instead of 'timeout'
- treat a turn as committed once delivery starts: /cancel re-checks
  deliveryStarted after the cancel RPC settles, and neither prompt path
  lets a late-settling cancel rewrite a delivered turn into cancelled
  (or follow a /clear cancellation with completed)
- tag failed lifecycle events with phase: agent vs delivery
- validate identity/memoryScope shape at config parse time instead of
  throwing an opaque TypeError on the first prompt of every session
- guard onPromptStart hooks, pass job.id to loop onPromptStart/End,
  append the boundary block after operator instructions, gate
  /who + /status identity lines on configured identity/memoryScope,
  cache the boundary prompt, and route error logs through
  lifecycleError()

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(qqbot): keep lifecycle mock in sync

* fix(channels): unify cancel state machine and adapter lifecycle edges

Address the remaining review findings on #6114:

- /cancel now delegates to requestActivePromptCancellation — one cancel
  state machine for slash command and adapter stop buttons; the helper
  refuses to claim success once delivery started and sanitizes its logs
- share isTerminalTaskLifecycleType from channel-base instead of four
  hand-rolled terminal checks
- DingTalk: only attach reactions for message ids seen inbound (loop job
  ids no longer trigger doomed emotion API calls), log reaction API
  failures, and recall reactions when a session dies
- Feishu: track userStopped on the card state so every wind-down path
  renders 已停止生成 after a Stop click, and pass terminal labels through
  updateCard's statusLabel param at the remaining baked-text sites
- Weixin: guard the typing .then() against a racing disconnect
- document onTaskLifecycle as the canonical hook (onPromptStart/End are
  back-compat), fix TS4111 bracket access in the DingTalk test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): keep failed terminal event when cancel settles post-delivery

Self-review follow-up: once delivery started, the catch paths no longer
reconcile a pending cancel — a late-resolving cancel RPC used to flip
cancelled=true there, suppressing the failed emit while the /cancel
handler (seeing deliveryStarted) also declined to emit, leaving a
started task with no terminal lifecycle event at all.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): close self-review findings on adapter lifecycle edges

- DingTalk: track inbound message ids in a capped insertion-ordered set
  instead of the TTL-swept dedup map, so a turn queued minutes behind a
  long predecessor still attaches its reaction
- Feishu: reset userStopped when the cancel RPC fails (later wind-down
  must render the real terminal status), and give handleStop's plain
  message fallback the same ---/label shape the strip regex expects

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): hold streamed chunks while a cancel is pending

Chunks arriving while a /cancel RPC is in flight were pushed straight
into the BlockStreamer, which can send a block on a size/paragraph
threshold before the cancel resolves — leaking output a successful
cancel can never recall. Hold the pending-window chunks instead: replay
them (block streaming + text_chunk transcript) when the cancel fails,
discard them when it succeeds. onResponseChunk stays live through the
window so adapter-accumulated display state has no permanent hole on a
failed cancel; adapters gate visible updates on their own stop flags.

Also stop passing the loop job id to onPromptStart/onPromptEnd (and the
/clear eviction path): the hook contract is inbound platform message
ids, and adapters act on them — cards and reactions keyed to a fake id.
Lifecycle events still carry job.id for correlation.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(feishu): strip every status-label layout from quoted-reply context

Replace the two $-anchored strip regexes in extractCardText with one
line-granular status-block filter. The anchored patterns missed four
layouts the cards actually render: the two-block truncation card
(notice block + label block), terminal labels joined mid-string before
a collapsible panel body, the 停止失败,请重试 label (never in the
alternation), and label-only stopped cards where the divider leads the
text. Tests now assert the real rendered shapes.

Also update the adapter-cancellation suppression test to the new
hold-and-replay chunk semantics from the base layer.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(feishu): ignore loop job ids in prompt hooks

* fix(channels): defer adapter chunks while cancel is pending

* fix(feishu): preserve bare status text in quotes

* fix(channels): release held chunks on failed cancel

* fix(feishu): preserve generated stop fallback status

* fix(telegram): clear typing on dead sessions

* fix(feishu): share status label definitions

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-02 11:01:07 +00:00

4.3 KiB

@qwen-code/channel-plugin-example

A reference channel plugin for Qwen Code. It connects to a WebSocket server and routes messages through the full channel pipeline (access control, session routing, agent bridge).

Use this package to:

  • Try out the channel plugin system — install it as an extension and run it with the built-in mock server
  • Use it as a starting point — fork the source to build your own channel adapter (see the Channel Plugin Developer Guide)

Quick start

1. Install the package

npm install @qwen-code/channel-plugin-example

The package ships a qwen-extension.json manifest, so it works as an extension out of the box:

qwen extensions link ./node_modules/@qwen-code/channel-plugin-example

3. Configure the channel

Add a channel entry to ~/.qwen/settings.json:

{
  "channels": {
    "my-plugin-test": {
      "type": "plugin-example",
      "serverWsUrl": "ws://localhost:9201",
      "senderPolicy": "open",
      "sessionScope": "user",
      "cwd": "/path/to/your/project"
    }
  }
}

4. Start the mock server

npx qwen-channel-plugin-example-server

The server prints the HTTP and WebSocket URLs. You can customize ports with environment variables:

HTTP_PORT=8080 WS_PORT=8081 npx qwen-channel-plugin-example-server

5. Start the channel

In a separate terminal:

qwen channel start my-plugin-test

Or run the same adapter under the experimental daemon-managed channel worker:

cd /path/to/your/project
qwen serve --channel my-plugin-test

qwen serve --channel requires the channel's configured cwd to resolve to the daemon workspace.

6. Send a message

curl -sX POST http://localhost:9200/message \
  -H 'Content-Type: application/json' \
  -d '{"senderId":"user1","senderName":"Tester","text":"What is 2+2?"}'

You should get a JSON response with the agent's reply.

How it works

Mock Server (HTTP + WS)
  ↕ WebSocket
MockPluginChannel (this package)
  → Envelope → ChannelBase.handleInbound()
    → SenderGate → SessionRouter → ChannelAgentBridge.prompt()
      → qwen-code agent → model API
    ← response
  ← sendMessage() → WebSocket → Mock Server
  ← HTTP response

Building your own channel

See src/MockPluginChannel.ts for a working example. The key points:

  1. Extend ChannelBase and implement connect(), sendMessage(), disconnect()
  2. Build an Envelope from incoming platform messages and call this.handleInbound(envelope)
  3. Type the adapter constructor bridge parameter as ChannelAgentBridge
  4. Export a plugin object conforming to ChannelPlugin
  5. Add a qwen-extension.json manifest

AcpBridge is still the current standalone qwen channel start implementation. Plugin adapters should depend on the ChannelAgentBridge abstraction provided by @qwen-code/channel-base.

Existing TypeScript plugins that explicitly type the adapter constructor or factory bridge parameter as AcpBridge should change that annotation to ChannelAgentBridge. JavaScript plugins are unaffected at runtime.

qwen serve --channel <name> hosts the same plugin through a daemon-managed worker backed by DaemonChannelBridge. The worker is owned by qwen serve; stop the daemon to stop serve-managed channels.

Features you get for free

  • Block streaming — enable blockStreaming: "on" in config and the agent's response is automatically split into multiple messages at paragraph boundaries
  • Attachments — populate envelope.attachments with images/files and handleInbound() routes them to the agent (images as vision input, files as paths in the prompt)
  • Streaming hooks — override onResponseChunk() for progressive display (e.g., editing a message in-place)
  • Access control (allowlist, pairing, open), session routing, slash commands, crash recovery

Lifecycle status

The mock plugin protocol exposes streamed chunks and final outbound messages only. It does not model typing indicators, reactions, card updates, or any other status surface, so prompt and task lifecycle status are intentionally no-op for this example channel.

Full guide: Channel Plugin Developer Guide