Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a1eb677
feat(channels): describe manageable channel fields
qqqys Jul 19, 2026
e840b4b
feat(serve): persist revisioned channel settings
qqqys Jul 19, 2026
834cff7
feat(serve): manage channel instances through daemon
qqqys Jul 19, 2026
8e7bc32
fix(serve): scope channel lifecycle by workspace
qqqys Jul 19, 2026
62f6c70
fix(serve): contain targeted channel reloads
qqqys Jul 19, 2026
085d62e
feat(serve): expose workspace channel management
qqqys Jul 19, 2026
15ef818
fix(serve): harden channel management routes
qqqys Jul 19, 2026
97d81da
feat(sdk): add daemon channel management client
qqqys Jul 19, 2026
0c90ab9
feat(serve): persist channel startup selection
qqqys Jul 19, 2026
9c1c68b
docs(serve): describe channel management API
qqqys Jul 19, 2026
fb9b810
fix(serve): handle all channel startup selection
qqqys Jul 19, 2026
eba4002
fix(serve): reserve all channel instance name
qqqys Jul 19, 2026
2342abb
fix(serve): canonicalize reserved channel name
qqqys Jul 19, 2026
fd277aa
feat(channels): scope daemon channel state by workspace
qqqys Jul 19, 2026
240e945
fix(channels): validate portable state paths
qqqys Jul 19, 2026
1e8e55f
fix(channels): hash daemon state identifiers
qqqys Jul 19, 2026
c13c931
fix(channels): reject malformed state identifiers
qqqys Jul 19, 2026
92f5333
feat(weixin): expose browser QR authentication
qqqys Jul 19, 2026
eb68177
fix(weixin): harden browser QR authentication
qqqys Jul 19, 2026
fcd2319
feat(qqbot): expose browser QR authentication
qqqys Jul 19, 2026
e264f71
fix(qqbot): settle auth on caller abort
qqqys Jul 19, 2026
feb9ad3
feat(serve): manage channel QR auth sessions
qqqys Jul 19, 2026
0acca3c
fix(serve): bound channel auth session retention
qqqys Jul 19, 2026
758b953
feat(sdk): expose channel QR authentication
qqqys Jul 19, 2026
cc7a7b1
fix(channels): migrate legacy daemon credentials
qqqys Jul 19, 2026
db49d89
fix(sdk): update daemon browser bundle budget
qqqys Jul 19, 2026
c664188
docs(channels): describe browser QR authentication
qqqys Jul 19, 2026
039ef65
fix(weixin): avoid creating legacy state on fallback
qqqys Jul 19, 2026
0d9a833
feat(webui): add daemon channel management hook
qqqys Jul 19, 2026
1995338
fix(webui): reset channel resource on workspace change
qqqys Jul 19, 2026
6a78ad6
fix(webui): commit channel resource identity safely
qqqys Jul 19, 2026
d78c19b
feat(web-shell): add channel management page
qqqys Jul 19, 2026
f7d380a
fix(web-shell): harden channel management actions
qqqys Jul 19, 2026
b329ef3
fix(web-shell): make channel revision lock global
qqqys Jul 19, 2026
841f525
feat(web-shell): edit typed channel configuration
qqqys Jul 19, 2026
502262f
fix(serve): isolate channel webhook secrets
qqqys Jul 19, 2026
6181bd5
fix(web-shell): isolate channel webhook secrets
qqqys Jul 19, 2026
ba090ff
fix(serve): validate channel secret update shapes
qqqys Jul 19, 2026
ae579e5
fix(web-shell): require webhook secret after env removal
qqqys Jul 19, 2026
dc07350
feat(web-shell): authenticate channels with QR codes
qqqys Jul 19, 2026
a1e2ca4
fix(web-shell): bind channel auth to workspace
qqqys Jul 19, 2026
bd782ab
feat(web-shell): open channel management from settings
qqqys Jul 19, 2026
c997638
fix(web-shell): preserve channel auth across locale changes
qqqys Jul 19, 2026
94dbeca
fix(web-shell): restore channels settings focus
qqqys Jul 19, 2026
deae94f
fix(web-shell): scope channels to active workspace
qqqys Jul 19, 2026
5b029d2
test(web-shell): cover channel management flows
qqqys Jul 19, 2026
c84b886
fix(web-shell): bind channels to selected workspace
qqqys Jul 19, 2026
a849d6c
fix(cli): harden channel management mutations
qqqys Jul 19, 2026
476d7be
feat(web-shell): complete workspace channel management
qqqys Jul 22, 2026
fd67589
Merge remote-tracking branch 'upstream/main' into codex/issue-7209-ch…
qqqys Jul 22, 2026
c673ede
docs(serve): document channel auth capability
qqqys Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 152 additions & 1 deletion docs/developers/daemon/15-channel-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,165 @@ sequenceDiagram

### Runtime selection and settings reload

The long-lived `ChannelWorkerManager` owns the committed daemon selection and workspace-grouped supervisors. A daemon may boot without `--channel`; the first strict-gated `PUT /workspace/channel` dynamically loads the channel runtime, reserves the service pidfile, resolves workspace ownership, and starts the selected workers. `GET /workspace/channel` reads the manager snapshot and `DELETE /workspace/channel` stops it idempotently. SDK helpers are `getChannelWorkerControl()`, `setChannelWorkerSelection()`, and `stopChannelWorker()`; the CLI entry is `qwen channel set` plus remote `status` and `stop` variants.
The long-lived `ChannelWorkerManager` owns the committed daemon selection and workspace-grouped supervisors. A daemon may boot without an effective selection when neither `--channel` nor the primary workspace's `serve.channels` selects an instance; the first strict-gated `PUT /workspace/channel` then dynamically loads the channel runtime, reserves the service pidfile, resolves workspace ownership, and starts the selected workers. `GET /workspace/channel` reads the manager snapshot and `DELETE /workspace/channel` stops it idempotently. SDK helpers are `getChannelWorkerControl()`, `setChannelWorkerSelection()`, and `stopChannelWorker()`; the CLI entry is `qwen channel set` plus remote `status` and `stop` variants.

The daemon reads channel settings from `settings.json` when each worker starts (`packages/cli/src/commands/channel/daemon-worker.ts` → `loadSettings` → `loadChannelsConfig`). `POST /workspace/channel/reload` re-reads those settings and force-reconciles the committed selection. All lifecycle mutations share one FIFO lane. Unchanged workspace groups survive ordinary selection replacement; changed groups stop and start sequentially while the serve-owned PID lease remains held.

If a replacement fails, newly started workers are stopped and old workers are restored before the request returns. A supervisor that cannot observe exit after SIGTERM and SIGKILL retains its child reference and fails stop; the manager keeps the PID lease and never starts a second worker. Webhook configuration and routing change only when selection commit succeeds. Runtime selections are process-local and disappear on daemon restart.

Adapter `connect()` failures are reported separately from worker lifecycle errors. The worker sends each bounded, credential-redacted failure over startup IPC and waits for a supervisor acknowledgement before trying the next adapter. A partially connected worker remains running and exposes `startupFailures` in its snapshot. If every adapter in a dynamic attempt fails, the `502 channel_worker_start_failed` response carries workspace-annotated attempted failures while `state` reflects the rollback result; subsequent GET responses do not retain the attempt. Daemon boot with no connected adapter remains fail-fast. The optional adapter `code` is diagnostic only, and the current `phase` is `connect`.

### Phase 1 configuration management contract

Configuration management is a separate projection over workspace settings and
the existing `ChannelWorkerManager`; it is not a second lifecycle owner. The
`channel_management` capability is advertised only when the service resolver
is wired.

Primary routes use `/workspace`; qualified routes replace that prefix with
`/workspaces/:workspace`, where the selector is an encoded canonical cwd:

```text
GET /workspace/channel-types
GET /workspace/channels
PUT /workspace/channels/:name
DELETE /workspace/channels/:name
PUT /workspace/channels/:name/startup
POST /workspace/channels/:name/start
POST /workspace/channels/:name/stop
POST /workspace/channels/:name/restart
```

Both forms resolve and require a trusted `WorkspaceRuntime` before resolving
its management service. Unknown, ambiguous, untrusted, draining, or removed
targets fail closed; qualified routes never reuse the primary service. Every
mutation uses the strict bearer gate and validates the client ID against the
resolved runtime. Instance names must be portable filesystem components of at
most 255 UTF-8 bytes.

`GET .../channel-types` returns the serializable projection of
`ChannelPlugin.management`: type, display name, `manageable`, safe field
descriptors, and declared `credentials` or `qr` auth modes. It never exposes
`createChannel`. A plugin without management metadata remains runnable through
the existing Channel paths but cannot be written through this API because its
secret fields cannot be identified safely.

`GET .../channels` returns:

```ts
interface DaemonChannelsSnapshot {
revision: string;
instances: Record<
string,
{
name: string;
config: Record<string, unknown>; // descriptor-declared secrets removed
secrets: Record<
string,
{
present: boolean;
source?: 'literal' | 'environment';
}
>;
startsWithServe: boolean;
runtime: {
state: 'stopped' | 'starting' | 'connected' | 'partial' | 'error';
lastError?: string;
};
}
>;
}
```

The revision is a deterministic SHA-256 digest of the workspace-scope
`channels` map and ordered `serve.channels` list. PUT and DELETE require the
latest value as `expectedRevision`; mismatch returns
`409 channel_settings_conflict` before writing.

Instance names must be non-empty portable filesystem components of at most 255
UTF-8 bytes and must not trim to the reserved startup sentinel `all`. Portable
components exclude path separators, control and Windows-forbidden characters,
trailing dots or spaces, and Windows device names. The management service uses
the channel-selection canonicalization as a domain invariant for configuration,
lifecycle, and per-instance startup mutations; routes map
`invalid_channel_instance_name` to 400. List may still expose a legacy
`channels.all` entry, and DELETE alone accepts that name for cleanup.

PUT accepts non-secret fields under `config` and descriptor-declared secret
fields only under `secrets`:

```json
{
"expectedRevision": "<latest revision>",
"config": { "type": "telegram" },
"secrets": {
"token": { "operation": "replace", "value": "$TELEGRAM_BOT_TOKEN" }
}
}
```

Secret operations are explicit: `preserve` retains the prior value, `replace`
requires a non-empty string, and `clear` removes it. Omitted descriptor secret
operations default to Preserve. A secret key under `config`, or a `secrets`
key not declared secret by the selected plugin, is rejected with
`channel_settings_invalid_secret`. Read responses expose presence/source only;
resolved environment values never enter the response.

The persistence/lifecycle ordering is intentional:

- PUT persists the workspace configuration first. If active, it calls
`reloadWorkspace(workspaceCwd, name)`. Failure retains the new settings,
removes the failed instance from committed runtime selection, and retains a
bounded, credential-redacted instance diagnostic.
- DELETE confirms Stop before removing settings and also removes the name from
`serve.channels`. Unconfirmed exit leaves both untouched. Deleting a legacy
`channels.all` entry, including a key with surrounding whitespace, never
treats the sentinel as a runtime instance. It canonicalizes the saved
sentinel to `serve.channels: ['all']` while other selectable configurations
remain and writes `[]` when none remain.
- PUT `.../:name/startup` requires an own configured instance, then adds its
name once to or removes it from the ordered `serve.channels` list through
the same revision check. It does not touch the manager or the instance
configuration. With the `all` sentinel active, every configured instance
projects `startsWithServe: true`; enabling one is a revision-checked no-op,
while disabling one expands the sentinel to all other selectable configured
names in persisted object order (or `[]`). The service never writes a mixed
`['all', ...]` list.
- Start adds the name to the manager's ordered, process-local committed
selection after unique workspace ownership preflight. Stop removes it;
Restart performs the same targeted owning-workspace reload as an active PUT.
None of these runtime actions edits `serve.channels`.

`WorkspaceChannelSettingsStore.setStartupNames()` persists the ordered
workspace `serve.channels` list separately from `channels.<name>`, and the
snapshot exposes list membership as `startsWithServe`. Merely configuring an
instance never opts it into startup.

`commands/serve.ts` resolves startup selection in this order: explicit
`argv.channel`, the primary workspace-scope `serve.channels`, then disabled.
It sends either source through `normalizeServeChannelSelection`, so trimming,
first-occurrence deduplication, and `all` exclusivity are identical. An absent
or empty saved list disables startup, and configured instances alone never
imply startup. Resolution is read-only: an explicit CLI selection remains
process-local and never rewrites the stored list. Only the primary workspace's
list is consulted at process boot; qualified secondary lists remain scoped to
their workspace and apply when that workspace is launched as primary.

The TypeScript SDK mirrors both scopes. `DaemonClient` targets primary routes;
`client.workspaceByCwd(cwd)` returns a `WorkspaceDaemonClient` for qualified
routes. Both expose `workspaceChannelTypes`, `workspaceChannels`,
`upsertWorkspaceChannel`, `deleteWorkspaceChannel`,
`setWorkspaceChannelStartup`, `startWorkspaceChannel`,
`stopWorkspaceChannel`, and `restartWorkspaceChannel`. Mutation helpers use
the existing extended Channel control timeout and propagate stable HTTP
failures as `DaemonHttpError`.

Phase 1 stops at descriptors, sanitized configuration CRUD, revisions,
instance lifecycle, and SDK helpers. `auth: ['qr']` on QQ and WeChat is
forward-looking metadata only. Browser QR auth sessions, the `channel_auth`
capability, credential commit/storage isolation, and the Web Shell Channels UI
belong to later phases and are not implemented by this contract.

## Dependencies

- `packages/channels/base/` — `ChannelBase`, `DaemonChannelBridge`, `types.ts` (`ChannelConfig`, `Envelope`, `SessionScope`, `ChannelPlugin`).
Expand Down
2 changes: 2 additions & 0 deletions docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ operator diagnostic snapshot documented below.
| `workspace_reload` | workspace reload support is available in the embedded route configuration. |
| `channel_reload` | a daemon-managed channel worker manager is enabled and can reload its current selection. |
| `channel_control` | daemon-managed channel worker runtime control is wired. |
| `channel_management` | workspace-scoped Channel configuration and instance lifecycle management are wired. |
| `channel_auth` | workspace-scoped Channel QR authentication sessions are wired. |
| `multi_workspace_sessions` | more than one workspace runtime is registered, so session creation can select a trusted runtime by cwd. |
| `multi_workspace_session_rewind` | more than one workspace runtime is registered; singular live-session rewind routes resolve the owning runtime. |
| `multi_workspace_session_shell` | more than one workspace runtime is registered and session shell execution is explicitly enabled; singular REST shell resolves the owning runtime. |
Expand Down
26 changes: 26 additions & 0 deletions docs/users/features/channels/qqbot.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ qwen channel start my-qq
# Scan the QR code in the terminal with your QQ app
```

### Authenticate from a daemon client

If `qwen serve` advertises the `channel_auth` capability, a browser or SDK
client can begin authentication for a configured QQ instance, display the
daemon-rendered QR SVG, poll until the session is `ready`, and explicitly
commit it. App credentials remain only in daemon memory until Commit, and the
session expires ten minutes after Begin. Cancellation, workspace removal,
daemon shutdown, or expiry discards uncommitted credentials. Commit writes to
that exact instance's workspace-scoped daemon state and does not start or
restart the channel. See
[Authenticate QQ and WeChat from a daemon client](../../qwen-serve#authenticate-qq-and-wechat-from-a-daemon-client)
for routes, SDK helpers, ownership, and QR response security.

Daemon startup normally reads only the scoped credential file. It may read the
exact legacy `~/.qwen/channels/<safe-name>-credentials.json` file read-only
only when daemon metadata proves this is the primary trusted workspace and the
workspace's complete `channels` map contains exactly one configured QQ
instance. Selecting one QQ instance while another is configured does not
qualify; secondary, untrusted, or ambiguous workspaces never fall back. Scoped
credentials take precedence; missing or corrupt scoped credentials may use the
legacy name file only with that proof. The legacy file is not deleted or
rewritten. A later successful browser Commit writes the scoped file, so the
credential is copied only after a successful save. Standalone QR login and its
legacy name-based storage remain unchanged.

### Manual Configuration (Developer Portal)

You can also use credentials from the [QQ Bot Open Platform](https://q.qq.com/) developer portal if you already have an app registered there:
Expand Down Expand Up @@ -167,6 +192,7 @@ Token refresh continues across WebSocket reconnects — the channel never goes o

- The QR code is displayed in the terminal. Scan it with your QQ mobile app (Me → Scan)
- If the QR code expires (typically after a few minutes), restart the channel to get a new one
- For daemon browser authentication, refetch the QR when `qrRevision` changes and Commit only after the session reports `ready`; the whole auth session expires after ten minutes

### Markdown messages appear as plain text

Expand Down
29 changes: 29 additions & 0 deletions docs/users/features/channels/weixin.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ Add the channel to `~/.qwen/settings.json`:

Note: WeChat channels do not use a `token` field — credentials come from the QR login step.

### Authenticate from a daemon client

If `qwen serve` advertises the `channel_auth` capability, a browser or SDK
client can start an auth session for a configured WeChat instance, render the
daemon-provided QR SVG, poll until the session is `ready`, and explicitly
commit it. The confirmed account remains only in daemon memory until Commit;
the session expires ten minutes after it starts. Cancelling it, removing its
workspace, shutting down the daemon, or allowing it to expire discards the
uncommitted account. Commit writes the account to that exact instance's
workspace-scoped daemon state and does not start or restart the channel. See
[Authenticate QQ and WeChat from a daemon client](../../qwen-serve#authenticate-qq-and-wechat-from-a-daemon-client)
for routes, SDK helpers, ownership, and QR response security.

Daemon startup does not normally read the standalone
`~/.qwen/channels/weixin/account.json` file (or the directory selected by
`WEIXIN_STATE_DIR`). It may use that singleton legacy account read-only only
when daemon metadata proves this is the primary trusted workspace and the
workspace's complete `channels` map contains exactly one configured WeChat
instance. Selecting one WeChat instance while another is configured does not
qualify, and secondary, untrusted, or ambiguous workspaces never fall back.
Scoped state takes precedence; missing or corrupt scoped state may use the
legacy account only with that proof. The legacy file is not deleted or
rewritten. A later successful browser Commit writes the scoped account, so the
credential is copied only after a successful save. The standalone
`qwen channel configure-weixin` and no-argument account lookup remain
unchanged.

### 3. Start the channel

```bash
Expand Down Expand Up @@ -89,6 +116,8 @@ WeChat channels support all the standard channel options (see [Channel Overview]
### "WeChat account not configured"

Run `qwen channel configure-weixin` to log in via QR code first.
For a daemon-managed instance, use its browser auth session and Commit before
starting or restarting the instance.

### "Session expired (errcode -14)"

Expand Down
Loading
Loading