mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: add channel status filtering (#80706)
Summary: - Add `openclaw channels status --channel <name>` filtering through CLI, gateway protocol, and fallback status rendering. - Document the BlueBubbles-to-iMessage cutover path so operators can probe iMessage without starting both monitors. - Refresh generated Swift protocol model for the new optional channel status parameter. Verification: - `pnpm test src/gateway/server-methods/channels.status.test.ts src/commands/channels.status.command-flow.test.ts src/cli/program/routes.test.ts -- --reporter=verbose` - `CI=true pnpm check:docs` - `pnpm protocol:check` - `git diff --check` - `node scripts/check-changelog-attributions.mjs` - CI head `45b27e3866`: focused/docs/protocol shards green locally; GitHub broad/scanner jobs queued for runners at merge attempt time; `Real behavior proof` failure is the maintainer-ignorable external-real-proof complaint.
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC.
|
||||
- ACP: expose Gateway session lineage metadata through ACP session listings and session info snapshots so clients can render subagent graphs without private Gateway side channels. (#73458) Thanks @samzong.
|
||||
- Channels/iMessage: add `openclaw channels status --channel <name>` filtering and document the BlueBubbles-to-imsg cutover path so operators can probe iMessage without starting both channel monitors. (#80706) Thanks @omarshahine.
|
||||
- CI: add a non-blocking `plugin-inspector-advisory` artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate.
|
||||
- Runtime/Fly: detect Fly Machines as container environments from their runtime env vars, so gateway bind and Bonjour defaults match remote container launches. (#80209) Thanks @liorb-mountapps.
|
||||
- Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to `/edit` with `image_urls` array, enforce NB2 edit geometry using `aspect_ratio` and `resolution` params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007.
|
||||
|
||||
@@ -3627,18 +3627,22 @@ public struct TalkSpeakResult: Codable, Sendable {
|
||||
public struct ChannelsStatusParams: Codable, Sendable {
|
||||
public let probe: Bool?
|
||||
public let timeoutms: Int?
|
||||
public let channel: String?
|
||||
|
||||
public init(
|
||||
probe: Bool?,
|
||||
timeoutms: Int?)
|
||||
timeoutms: Int?,
|
||||
channel: String?)
|
||||
{
|
||||
self.probe = probe
|
||||
self.timeoutms = timeoutms
|
||||
self.channel = channel
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case probe
|
||||
case timeoutms = "timeoutMs"
|
||||
case channel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
"source": "Coming from BlueBubbles",
|
||||
"target": "Coming from BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "BlueBubbles removal and the imsg iMessage path",
|
||||
"target": "BlueBubbles removal and the imsg iMessage path"
|
||||
},
|
||||
{
|
||||
"source": "BlueBubbles",
|
||||
"target": "BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "Configuration reference - iMessage",
|
||||
"target": "Configuration reference - iMessage"
|
||||
},
|
||||
{
|
||||
"source": "Pairing",
|
||||
"target": "配对"
|
||||
|
||||
79
docs/announcements/bluebubbles-imessage.md
Normal file
79
docs/announcements/bluebubbles-imessage.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
summary: "BlueBubbles support was removed from OpenClaw. Use the bundled iMessage plugin with imsg for new and migrated iMessage setups."
|
||||
read_when:
|
||||
- You used the old BlueBubbles channel and need to move to iMessage
|
||||
- You are choosing the supported OpenClaw iMessage setup
|
||||
- You need a short explanation of the BlueBubbles removal
|
||||
title: "BlueBubbles removal and the imsg iMessage path"
|
||||
---
|
||||
|
||||
# BlueBubbles removal and the imsg iMessage path
|
||||
|
||||
OpenClaw no longer ships the BlueBubbles channel. iMessage support now runs through the bundled `imessage` plugin, which starts [`imsg`](https://github.com/steipete/imsg) locally or through an SSH wrapper and talks JSON-RPC over stdin/stdout.
|
||||
|
||||
If your config still contains `channels.bluebubbles`, migrate it to `channels.imessage`. The legacy `/channels/bluebubbles` docs URL redirects to [Coming from BlueBubbles](/channels/imessage-from-bluebubbles), which has the full config translation table and cutover checklist.
|
||||
|
||||
## What changed
|
||||
|
||||
- There is no BlueBubbles HTTP server, webhook route, REST password, or BlueBubbles plugin runtime in the supported OpenClaw iMessage path.
|
||||
- OpenClaw reads and watches Messages through `imsg` on the Mac where Messages.app is signed in.
|
||||
- Basic send, receive, history, and media use the normal `imsg` surfaces and macOS permissions.
|
||||
- Advanced actions such as threaded replies, tapbacks, edit, unsend, effects, read receipts, typing indicators, and group management require `imsg launch` with the private API bridge available.
|
||||
- Linux and Windows gateways can still use iMessage by setting `channels.imessage.cliPath` to an SSH wrapper that runs `imsg` on the signed-in Mac.
|
||||
|
||||
## What to do
|
||||
|
||||
1. Install and verify `imsg` on the Messages Mac:
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg --version
|
||||
imsg chats --limit 3
|
||||
imsg rpc --help
|
||||
```
|
||||
|
||||
2. Grant Full Disk Access and Automation permissions to the process context that runs `imsg` and OpenClaw.
|
||||
|
||||
3. Translate the old config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
},
|
||||
includeAttachments: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
4. Restart the gateway and verify:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
5. Test DMs, groups, attachments, and any private API actions you depend on before deleting your old BlueBubbles server.
|
||||
|
||||
## Migration notes
|
||||
|
||||
- `channels.bluebubbles.serverUrl` and `channels.bluebubbles.password` have no iMessage equivalent.
|
||||
- `channels.bluebubbles.allowFrom`, `groupAllowFrom`, `groups`, `includeAttachments`, attachment roots, media size limits, chunking, and action toggles have iMessage equivalents.
|
||||
- `channels.imessage.includeAttachments` is still off by default. Set it explicitly if you expect inbound photos, voice memos, videos, or files to reach the agent.
|
||||
- With `groupPolicy: "allowlist"`, copy the old `groups` block, including any `"*"` wildcard entry. Group sender allowlists and the group registry are separate gates.
|
||||
- ACP bindings that matched `channel: "bluebubbles"` must be changed to `channel: "imessage"`.
|
||||
- Old BlueBubbles session keys do not become iMessage session keys. Pairing approvals carry over by handle, but conversation history under BlueBubbles session keys does not.
|
||||
|
||||
## See also
|
||||
|
||||
- [Coming from BlueBubbles](/channels/imessage-from-bluebubbles)
|
||||
- [iMessage](/channels/imessage)
|
||||
- [Configuration reference - iMessage](/gateway/config-channels#imessage)
|
||||
@@ -11,6 +11,22 @@ The bundled `imessage` plugin now reaches the same private API surface as BlueBu
|
||||
|
||||
BlueBubbles support was removed. OpenClaw supports iMessage through `imsg` only. This guide is for migrating old `channels.bluebubbles` configs to `channels.imessage`; there is no other supported migration path.
|
||||
|
||||
<Note>
|
||||
For the short announcement and operator summary, see [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage).
|
||||
</Note>
|
||||
|
||||
## Migration checklist
|
||||
|
||||
Use this checklist when you already know your old BlueBubbles config and want the shortest safe path:
|
||||
|
||||
1. Verify `imsg` directly on the Mac that runs Messages.app (`imsg chats`, `imsg history`, `imsg send`, and `imsg rpc --help`).
|
||||
2. Copy behavior keys from `channels.bluebubbles` to `channels.imessage`: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `includeAttachments`, `attachmentRoots`, `mediaMaxMb`, `textChunkLimit`, `coalesceSameSenderDms`, and `actions`.
|
||||
3. Drop transport keys that no longer exist: `serverUrl`, `password`, webhook URLs, and BlueBubbles server setup.
|
||||
4. If the Gateway is not running on the Messages Mac, set `channels.imessage.cliPath` to an SSH wrapper and set `remoteHost` for remote attachment fetches.
|
||||
5. With the Gateway stopped, enable `channels.imessage`, then run `openclaw channels status --probe --channel imessage`.
|
||||
6. Test one DM, one allowed group, attachments if enabled, and every private API action you expect the agent to use.
|
||||
7. Delete the BlueBubbles server and old `channels.bluebubbles` config after the iMessage path is verified.
|
||||
|
||||
## When this migration makes sense
|
||||
|
||||
- You already run `imsg` on the same Mac (or one reachable over SSH) where Messages.app is signed in.
|
||||
@@ -60,13 +76,13 @@ BlueBubbles support was removed. OpenClaw supports iMessage through `imsg` only.
|
||||
|
||||
`imsg launch` requires SIP to be disabled. Basic send, history, and watch work without `imsg launch`; advanced actions do not.
|
||||
|
||||
4. Verify the bridge through OpenClaw:
|
||||
4. After you add an enabled `channels.imessage` config, verify the bridge through OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
You want `imessage.privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions).
|
||||
You want `imessage.privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions). `channels status --probe` only probes configured, enabled accounts.
|
||||
|
||||
5. Snapshot your config:
|
||||
|
||||
@@ -143,7 +159,7 @@ If the gateway logs `imessage: dropping group message from chat_id=<id>` or the
|
||||
|
||||
## Step-by-step
|
||||
|
||||
1. Add an iMessage block alongside the existing BlueBubbles block. Keep the old block only as a copy source until the new path is verified:
|
||||
1. Add an iMessage block alongside the existing BlueBubbles block. Keep it disabled while the Gateway is still routing BlueBubbles traffic:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -153,7 +169,7 @@ If the gateway logs `imessage: dropping group message from chat_id=<id>` or the
|
||||
// ... existing config ...
|
||||
},
|
||||
imessage: {
|
||||
enabled: false, // turn on after the dry run below
|
||||
enabled: false,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
|
||||
@@ -173,17 +189,17 @@ If the gateway logs `imessage: dropping group message from chat_id=<id>` or the
|
||||
}
|
||||
```
|
||||
|
||||
2. **Dry-run probe** — start the gateway and confirm iMessage reports healthy:
|
||||
2. **Probe before traffic matters** — stop the Gateway, temporarily enable the iMessage block, and confirm iMessage reports healthy from the CLI:
|
||||
|
||||
```bash
|
||||
openclaw gateway
|
||||
openclaw channels status
|
||||
openclaw channels status --probe # expect imessage.privateApi.available: true
|
||||
openclaw gateway stop
|
||||
# edit config: channels.imessage.enabled = true
|
||||
openclaw channels status --probe --channel imessage # expect imessage.privateApi.available: true
|
||||
```
|
||||
|
||||
Because `imessage.enabled` is still `false`, no inbound iMessage traffic is routed yet — but `--probe` exercises the bridge so you catch permission/install issues before the cutover.
|
||||
`channels status --probe` only probes configured, enabled accounts. Do not restart the Gateway with both BlueBubbles and iMessage enabled unless you intentionally want both channel monitors running. If you are not cutting over immediately, set `channels.imessage.enabled` back to `false` before restarting the Gateway. Use the direct `imsg` commands in [Before you start](#before-you-start) to validate the Mac before enabling OpenClaw traffic.
|
||||
|
||||
3. **Cut over.** Remove the BlueBubbles config and enable iMessage in one config edit:
|
||||
3. **Cut over.** Once the enabled iMessage account reports healthy, remove the BlueBubbles config and keep iMessage enabled:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -236,6 +252,7 @@ The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0
|
||||
|
||||
## Related
|
||||
|
||||
- [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) — short announcement and operator summary.
|
||||
- [iMessage](/channels/imessage) — full iMessage channel reference, including `imsg launch` setup and capability detection.
|
||||
- `/channels/bluebubbles` — legacy URL that redirects to this migration guide.
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow.
|
||||
|
||||
@@ -13,7 +13,7 @@ For OpenClaw iMessage deployments, use `imsg` on a signed-in macOS Messages host
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only.
|
||||
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only. Start with [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) for the short announcement, or [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) for the full migration table.
|
||||
</Warning>
|
||||
|
||||
Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port). Advanced actions require `imsg launch` and a successful private API probe.
|
||||
@@ -780,6 +780,7 @@ openclaw channels status --probe --channel imessage
|
||||
## Related
|
||||
|
||||
- [Channels Overview](/channels) — all supported channels
|
||||
- [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) — announcement and migration summary
|
||||
- [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) — config translation table and step-by-step cutover
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow
|
||||
- [Groups](/channels/groups) — group chat behavior and mention gating
|
||||
|
||||
@@ -32,7 +32,7 @@ openclaw channels logs --channel all
|
||||
|
||||
## Status / capabilities / resolve / logs
|
||||
|
||||
- `channels status`: `--probe`, `--timeout <ms>`, `--json`
|
||||
- `channels status`: `--channel <name>`, `--probe`, `--timeout <ms>`, `--json`
|
||||
- `channels capabilities`: `--channel <name>`, `--account <id>` (only with `--channel`), `--target <dest>`, `--timeout <ms>`, `--json`
|
||||
- `channels resolve`: `<entries...>`, `--channel <name>`, `--account <id>`, `--kind <auto|user|group>`, `--json`
|
||||
- `channels logs`: `--channel <name|all>`, `--lines <n>`, `--json`
|
||||
|
||||
@@ -1050,7 +1050,7 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["channels/index"]
|
||||
"pages": ["channels/index", "announcements/bluebubbles-imessage"]
|
||||
},
|
||||
{
|
||||
"group": "Mainstream messaging",
|
||||
|
||||
@@ -588,7 +588,7 @@ When Mattermost native commands are enabled:
|
||||
|
||||
OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required. This is the preferred path for new OpenClaw iMessage setups when the host can grant Messages database and Automation permissions.
|
||||
|
||||
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only.
|
||||
BlueBubbles support was removed. `channels.bluebubbles` is not a supported runtime config surface on current OpenClaw. Migrate old configs to `channels.imessage`; use [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) for the short version and [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) for the full translation table.
|
||||
|
||||
If the Gateway is not running on the signed-in Messages Mac, keep `channels.imessage.enabled=true` and set `channels.imessage.cliPath` to an SSH wrapper that runs `imsg "$@"` on that Mac. The default local `imsg` path is macOS-only.
|
||||
|
||||
@@ -609,6 +609,17 @@ If the Gateway is not running on the signed-in Messages Mac, keep `channels.imes
|
||||
mediaMaxMb: 16,
|
||||
service: "auto",
|
||||
region: "US",
|
||||
actions: {
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
sendWithEffect: true,
|
||||
sendAttachment: true,
|
||||
},
|
||||
catchup: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -622,6 +633,10 @@ If the Gateway is not running on the signed-in Messages Mac, keep `channels.imes
|
||||
- `attachmentRoots` and `remoteAttachmentRoots` restrict inbound attachment paths (default: `/Users/*/Library/Messages/Attachments`).
|
||||
- SCP uses strict host-key checking, so ensure the relay host key already exists in `~/.ssh/known_hosts`.
|
||||
- `channels.imessage.configWrites`: allow or deny iMessage-initiated config writes.
|
||||
- `channels.imessage.actions.*`: enable private API actions that are also gated by `imsg status` / `openclaw channels status --probe`.
|
||||
- `channels.imessage.includeAttachments` is off by default; set it to `true` before expecting inbound media in agent turns.
|
||||
- `channels.imessage.catchup.enabled`: opt in to replaying inbound messages that arrived while the Gateway was down.
|
||||
- `channels.imessage.groups`: group registry and per-group settings. With `groupPolicy: "allowlist"`, configure either explicit `chat_id` keys or a `"*"` wildcard entry so group messages can pass the registry gate.
|
||||
- Top-level `bindings[]` entries with `type: "acp"` can bind iMessage conversations to persistent ACP sessions. Use a normalized handle or explicit chat target (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`) in `match.peer.id`. Shared field semantics: [ACP Agents](/tools/acp-agents#persistent-channel-bindings).
|
||||
|
||||
<Accordion title="iMessage SSH wrapper example">
|
||||
|
||||
@@ -130,6 +130,7 @@ export async function registerChannelsCli(
|
||||
channels
|
||||
.command("status")
|
||||
.description("Show gateway channel status (use status --deep for local)")
|
||||
.option("--channel <name>", `Only show one channel (${formatCliChannelOptions(["all"])})`)
|
||||
.option("--probe", "Probe channel credentials", false)
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--json", "Output JSON", false)
|
||||
|
||||
@@ -257,10 +257,15 @@ export function parseChannelsListRouteArgs(argv: string[]) {
|
||||
|
||||
export function parseChannelsStatusRouteArgs(argv: string[]) {
|
||||
const timeout = parseOptionalFlagValue(argv, "--timeout");
|
||||
const channel = parseOptionalFlagValue(argv, "--channel");
|
||||
if (!timeout.ok) {
|
||||
return null;
|
||||
}
|
||||
if (!channel.ok) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
channel: channel.value,
|
||||
json: hasFlag(argv, "--json"),
|
||||
probe: hasFlag(argv, "--probe"),
|
||||
timeout: timeout.value,
|
||||
|
||||
@@ -138,12 +138,14 @@ describe("program routes", () => {
|
||||
"status",
|
||||
"--json",
|
||||
"--probe",
|
||||
"--channel",
|
||||
"imsg",
|
||||
"--timeout",
|
||||
"5000",
|
||||
]),
|
||||
).resolves.toBe(true);
|
||||
expect(channelsStatusCommandMock).toHaveBeenCalledWith(
|
||||
{ json: true, probe: true, timeout: "5000" },
|
||||
{ channel: "imsg", json: true, probe: true, timeout: "5000" },
|
||||
defaultRuntime,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -106,6 +106,7 @@ vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: () => mocks.listChannelPlugins(),
|
||||
getChannelPlugin: (channel: string) =>
|
||||
(mocks.listChannelPlugins() as Array<{ id: string }>).find((plugin) => plugin.id === channel),
|
||||
normalizeChannelId: (channel: string) => (channel === "imsg" ? "imessage" : channel),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/read-only.js", () => ({
|
||||
@@ -210,6 +211,23 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
mocks.listChannelPlugins.mockReturnValue([createTokenOnlyPlugin()]);
|
||||
});
|
||||
|
||||
it("passes a channel filter to the gateway status request", async () => {
|
||||
mocks.callGateway.mockResolvedValue({
|
||||
channelAccounts: { imessage: [] },
|
||||
channels: { imessage: {} },
|
||||
});
|
||||
const { runtime } = createCapturingTestRuntime();
|
||||
|
||||
await channelsStatusCommand({ channel: "imsg", json: true, probe: true }, runtime as never);
|
||||
|
||||
expect(mocks.callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "channels.status",
|
||||
params: { channel: "imsg", probe: true, timeoutMs: 30000 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps read-only fallback output when SecretRefs are unresolved", async () => {
|
||||
mocks.callGateway.mockRejectedValue(new Error("gateway closed"));
|
||||
mocks.requireValidConfigSnapshot.mockResolvedValue({ secretResolved: false, channels: {} });
|
||||
@@ -300,7 +318,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
});
|
||||
const { runtime, logs, errors } = createCapturingTestRuntime();
|
||||
|
||||
await channelsStatusCommand({ json: true, probe: false }, runtime as never);
|
||||
await channelsStatusCommand({ channel: "imsg", json: true, probe: false }, runtime as never);
|
||||
|
||||
expect(mocks.listChannelPlugins).not.toHaveBeenCalled();
|
||||
expect(mocks.listConfiguredChannelIdsForReadOnlyScope).toHaveBeenCalledOnce();
|
||||
@@ -322,6 +340,6 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
expect(payload.error).not.toContain("fallback-secret");
|
||||
expect(payload.gatewayReachable).toBe(false);
|
||||
expect(payload.configOnly).toBe(true);
|
||||
expect(payload.configuredChannels).toStrictEqual(["discord"]);
|
||||
expect(payload.configuredChannels).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
hasConfiguredUnavailableCredentialStatus,
|
||||
hasResolvedCredentialValue,
|
||||
} from "../../channels/account-snapshot-fields.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { listReadOnlyChannelPluginsForConfig } from "../../channels/plugins/read-only.js";
|
||||
import {
|
||||
buildChannelAccountSnapshot,
|
||||
@@ -33,7 +34,7 @@ type ChannelStatusPluginLabel = {
|
||||
export async function formatConfigChannelsStatusLines(
|
||||
cfg: OpenClawConfig,
|
||||
meta: { path?: string; mode?: "local" | "remote" },
|
||||
opts?: { sourceConfig?: OpenClawConfig },
|
||||
opts?: { sourceConfig?: OpenClawConfig; channel?: string },
|
||||
): Promise<string[]> {
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.warn("Gateway not reachable; showing config-only status."));
|
||||
@@ -63,10 +64,11 @@ export async function formatConfigChannelsStatusLines(
|
||||
});
|
||||
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
const requestedChannel = opts?.channel ? normalizeChannelId(opts.channel) : null;
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(cfg, {
|
||||
activationSourceConfig: sourceConfig,
|
||||
includeSetupFallbackPlugins: true,
|
||||
});
|
||||
}).filter((plugin) => !requestedChannel || plugin.id === requestedChannel);
|
||||
const visibleChannelIds = new Set<string>();
|
||||
for (const plugin of plugins) {
|
||||
visibleChannelIds.add(plugin.id);
|
||||
@@ -108,6 +110,9 @@ export async function formatConfigChannelsStatusLines(
|
||||
]),
|
||||
];
|
||||
for (const channelId of missingChannelIds) {
|
||||
if (requestedChannel && channelId !== requestedChannel) {
|
||||
continue;
|
||||
}
|
||||
if (visibleChannelIds.has(channelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { getConfiguredChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { formatConfigChannelsStatusLines } from "./status-config-format.js";
|
||||
|
||||
export type ChannelsStatusOptions = {
|
||||
channel?: string;
|
||||
json?: boolean;
|
||||
probe?: boolean;
|
||||
timeout?: string;
|
||||
@@ -205,6 +207,7 @@ export async function channelsStatusCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const timeoutMs = Number(opts.timeout ?? (opts.probe ? 30_000 : 10_000));
|
||||
const requestedChannel = opts.channel ? normalizeChannelId(opts.channel) : null;
|
||||
const statusLabel = opts.probe ? "Checking channel status (probe)…" : "Checking channel status…";
|
||||
const shouldLogStatus = opts.json !== true && !process.stderr.isTTY;
|
||||
if (shouldLogStatus) {
|
||||
@@ -217,12 +220,20 @@ export async function channelsStatusCommand(
|
||||
indeterminate: true,
|
||||
enabled: opts.json !== true,
|
||||
},
|
||||
async () =>
|
||||
await callGateway({
|
||||
method: "channels.status",
|
||||
params: { probe: Boolean(opts.probe), timeoutMs },
|
||||
async () => {
|
||||
const params: { channel?: string; probe: boolean; timeoutMs: number } = {
|
||||
probe: Boolean(opts.probe),
|
||||
timeoutMs,
|
||||
}),
|
||||
};
|
||||
if (opts.channel) {
|
||||
params.channel = opts.channel;
|
||||
}
|
||||
return await callGateway({
|
||||
method: "channels.status",
|
||||
params,
|
||||
timeoutMs,
|
||||
});
|
||||
},
|
||||
);
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, payload);
|
||||
@@ -259,7 +270,7 @@ export async function channelsStatusCommand(
|
||||
activationSourceConfig: cfg,
|
||||
env: process.env,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
}).filter((channelId) => !requestedChannel || channelId === requestedChannel),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -271,7 +282,7 @@ export async function channelsStatusCommand(
|
||||
path: snapshot.path,
|
||||
mode,
|
||||
},
|
||||
{ sourceConfig: cfg },
|
||||
{ sourceConfig: cfg, channel: opts.channel },
|
||||
)
|
||||
).join("\n"),
|
||||
);
|
||||
|
||||
@@ -577,6 +577,7 @@ export const ChannelsStatusParamsSchema = Type.Object(
|
||||
{
|
||||
probe: Type.Optional(Type.Boolean()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
channel: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
@@ -176,6 +176,58 @@ describe("channelsHandlers channels.status", () => {
|
||||
expect(probeArgs.cfg).toBe(autoEnabledConfig);
|
||||
});
|
||||
|
||||
it("filters channel status to a requested channel", async () => {
|
||||
const autoEnabledConfig = { autoEnabled: true };
|
||||
const whatsappProbe = vi.fn(async () => ({ ok: true }));
|
||||
const imessageProbe = vi.fn(async () => ({ ok: true }));
|
||||
mocks.applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] });
|
||||
mocks.buildChannelUiCatalog.mockImplementation((plugins: Array<{ id: string }>) => ({
|
||||
order: plugins.map((plugin) => plugin.id),
|
||||
labels: Object.fromEntries(plugins.map((plugin) => [plugin.id, plugin.id])),
|
||||
detailLabels: Object.fromEntries(plugins.map((plugin) => [plugin.id, plugin.id])),
|
||||
systemImages: {},
|
||||
entries: Object.fromEntries(plugins.map((plugin) => [plugin.id, { id: plugin.id }])),
|
||||
}));
|
||||
mocks.listChannelPlugins.mockReturnValue([
|
||||
{
|
||||
id: "whatsapp",
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
isEnabled: () => true,
|
||||
isConfigured: async () => true,
|
||||
},
|
||||
status: { probeAccount: whatsappProbe },
|
||||
},
|
||||
{
|
||||
id: "imessage",
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
isEnabled: () => true,
|
||||
isConfigured: async () => true,
|
||||
},
|
||||
status: { probeAccount: imessageProbe },
|
||||
},
|
||||
]);
|
||||
const respond = vi.fn();
|
||||
|
||||
await channelsHandlers["channels.status"](
|
||||
createOptions({ channel: "imessage", probe: true, timeoutMs: 1000 }, { respond }),
|
||||
);
|
||||
|
||||
expect(whatsappProbe).not.toHaveBeenCalled();
|
||||
expect(imessageProbe).toHaveBeenCalledOnce();
|
||||
const payload = requireRespondPayload(respond);
|
||||
expect(payload.channelOrder).toEqual(["imessage"]);
|
||||
expect(payload.channels).toEqual({
|
||||
imessage: expect.any(Object),
|
||||
});
|
||||
expect(payload.channelAccounts).toEqual({
|
||||
imessage: [expect.objectContaining({ accountId: "default" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves channel account rows when a live probe throws", async () => {
|
||||
const autoEnabledConfig = { autoEnabled: true };
|
||||
const probeAccount = vi.fn(async () => {
|
||||
|
||||
@@ -298,14 +298,28 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
const probe = (params as { probe?: boolean }).probe === true;
|
||||
const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs;
|
||||
const timeoutMs = resolveChannelsStatusTimeoutMs({ probe, timeoutMsRaw });
|
||||
const rawChannel = (params as { channel?: unknown }).channel;
|
||||
const requestedChannel =
|
||||
typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : undefined;
|
||||
const cfg = applyPluginAutoEnable({
|
||||
config: context.getRuntimeConfig(),
|
||||
env: process.env,
|
||||
}).config;
|
||||
const runtime = context.getRuntimeSnapshot();
|
||||
const plugins = listChannelPlugins();
|
||||
const selectedPlugins = requestedChannel
|
||||
? plugins.filter((plugin) => plugin.id === requestedChannel)
|
||||
: plugins;
|
||||
if (rawChannel !== undefined && !requestedChannel) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, `unknown channel: ${formatForLog(rawChannel)}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const pluginMap = new Map<ChannelId, ChannelPlugin>(
|
||||
plugins.map((plugin) => [plugin.id, plugin]),
|
||||
selectedPlugins.map((plugin) => [plugin.id, plugin]),
|
||||
);
|
||||
const statusWarnings: string[] = [];
|
||||
|
||||
@@ -461,7 +475,7 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
return { accounts, defaultAccountId, defaultAccount, resolvedAccounts };
|
||||
};
|
||||
|
||||
const uiCatalog = buildChannelUiCatalog(plugins);
|
||||
const uiCatalog = buildChannelUiCatalog(selectedPlugins);
|
||||
const payload: Record<string, unknown> = {
|
||||
ts: Date.now(),
|
||||
channelOrder: uiCatalog.order,
|
||||
@@ -478,7 +492,7 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
const accountsMap = payload.channelAccounts as Record<string, unknown>;
|
||||
const defaultAccountIdMap = payload.channelDefaultAccountId as Record<string, unknown>;
|
||||
const { results: channelResults } = await runTasksWithConcurrency({
|
||||
tasks: plugins.map((plugin) => async () => {
|
||||
tasks: selectedPlugins.map((plugin) => async () => {
|
||||
const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } =
|
||||
await buildChannelAccounts(plugin.id);
|
||||
const fallbackAccount =
|
||||
@@ -509,7 +523,7 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
return { pluginId: plugin.id, summary, accounts, defaultAccountId };
|
||||
}),
|
||||
limit: probe ? CHANNEL_STATUS_PROBE_CONCURRENCY : plugins.length || 1,
|
||||
limit: probe ? CHANNEL_STATUS_PROBE_CONCURRENCY : selectedPlugins.length || 1,
|
||||
});
|
||||
for (const result of channelResults) {
|
||||
if (result) {
|
||||
|
||||
Reference in New Issue
Block a user