Skip to content

Commit f042b53

Browse files
authored
fix(channels): preserve channel aliases in plugin probes
Key package-state probes, env/config presence, and read-only command defaults by channel id instead of manifest plugin id so alias-owned channel plugins keep setup/native-command detection working.
1 parent 592998a commit f042b53

8 files changed

Lines changed: 139 additions & 16 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
6060
### Fixes
6161

6262
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
63+
- Channels/plugins: key bundled package-state probes, env/config presence, and read-only command defaults by channel id instead of manifest plugin id, preserving setup and native-command detection for channel plugins whose package id differs from the channel alias. Thanks @vincentkoc.
6364
- Docker: prune package-excluded plugin dist directories from runtime images unless the build explicitly opts that plugin in, so official external plugins such as Feishu stay install-on-demand instead of shipping partial metadata without compiled runtime output. Fixes #77424. Thanks @vincentkoc.
6465
- Model switching: include the exact additive allowlist repair command when `/model ... --runtime ...` targets a blocked model, and make Telegram's model picker say that it changes only the session model while leaving the runtime unchanged. Thanks @vincentkoc.
6566
- Doctor/config: keep active `auth.profiles` metadata intact when `doctor --fix` strips stale secret fields from configs, repairing legacy `<provider>:default` API-key profile metadata when model fallbacks or explicit `model@profile` refs still depend on it. Fixes #77400.

src/channels/config-presence.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
99
import { hasNonEmptyString } from "../infra/outbound/channel-target.js";
1010
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
1111
import { isRecord } from "../utils.js";
12-
import { listBundledChannelPluginIds } from "./plugins/bundled-ids.js";
12+
import { listBundledChannelIds } from "./plugins/bundled-ids.js";
1313

1414
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
1515

@@ -121,7 +121,7 @@ export function listPotentialConfiguredChannelPresenceSignals(
121121
signals.push({ channelId, source });
122122
};
123123
const configuredChannelIds = new Set<string>();
124-
const channelIds = options.channelIds ?? listBundledChannelPluginIds(env);
124+
const channelIds = options.channelIds ?? listBundledChannelIds(env);
125125
const channelEnvPrefixes = listChannelEnvPrefixes(channelIds);
126126
const channels = isRecord(cfg.channels) ? cfg.channels : null;
127127
if (channels) {
@@ -165,7 +165,7 @@ function hasEnvConfiguredChannel(
165165
env: NodeJS.ProcessEnv,
166166
options: ChannelPresenceOptions = {},
167167
): boolean {
168-
const channelIds = options.channelIds ?? listBundledChannelPluginIds(env);
168+
const channelIds = options.channelIds ?? listBundledChannelIds(env);
169169
const channelEnvPrefixes = listChannelEnvPrefixes(channelIds);
170170
for (const [key, value] of Object.entries(env)) {
171171
if (!hasNonEmptyString(value)) {

src/channels/plugins/bundled-ids.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ export function listBundledChannelPluginIdsForRoot(
1010
.toSorted((left, right) => left.localeCompare(right));
1111
}
1212

13+
export function listBundledChannelIdsForRoot(
14+
_packageRoot: string,
15+
env: NodeJS.ProcessEnv = process.env,
16+
): string[] {
17+
return listChannelCatalogEntries({ origin: "bundled", env })
18+
.map((entry) => entry.channel.id)
19+
.filter((channelId): channelId is string => Boolean(channelId))
20+
.toSorted((left, right) => left.localeCompare(right));
21+
}
22+
1323
export function listBundledChannelPluginIds(env: NodeJS.ProcessEnv = process.env): string[] {
1424
return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env);
1525
}
26+
27+
export function listBundledChannelIds(env: NodeJS.ProcessEnv = process.env): string[] {
28+
return listBundledChannelIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env);
29+
}

src/channels/plugins/bundled-root-caches.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ describe("bundled root-aware plugin lookups", () => {
6262
listChannelCatalogEntries: (params?: { env?: NodeJS.ProcessEnv }) => {
6363
const activeRoot = params?.env?.OPENCLAW_BUNDLED_PLUGINS_DIR;
6464
if (activeRoot === rootA.pluginsDir) {
65-
return [{ pluginId: "alpha" }];
65+
return [{ pluginId: "alpha", channel: { id: "alpha-chat" } }];
6666
}
6767
if (activeRoot === rootB.pluginsDir) {
68-
return [{ pluginId: "beta" }];
68+
return [{ pluginId: "beta", channel: { id: "beta-chat" } }];
6969
}
7070
return [];
7171
},
@@ -78,9 +78,11 @@ describe("bundled root-aware plugin lookups", () => {
7878

7979
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootA.pluginsDir;
8080
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["alpha"]);
81+
expect(bundledIds.listBundledChannelIds()).toEqual(["alpha-chat"]);
8182

8283
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootB.pluginsDir;
8384
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["beta"]);
85+
expect(bundledIds.listBundledChannelIds()).toEqual(["beta-chat"]);
8486
});
8587

8688
it("reads bootstrap plugins from the active bundled root without re-importing", async () => {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { PluginChannelCatalogEntry } from "../../plugins/channel-catalog-registry.js";
3+
import {
4+
hasBundledChannelPackageState,
5+
listBundledChannelIdsForPackageState,
6+
} from "./package-state-probes.js";
7+
8+
const listChannelCatalogEntriesMock = vi.hoisted(() => vi.fn());
9+
10+
vi.mock("../../plugins/channel-catalog-registry.js", () => ({
11+
listChannelCatalogEntries: listChannelCatalogEntriesMock,
12+
}));
13+
14+
function makeBundledChannelCatalogEntry(params: {
15+
pluginId: string;
16+
channelId: string;
17+
}): PluginChannelCatalogEntry {
18+
return {
19+
pluginId: params.pluginId,
20+
origin: "bundled",
21+
rootDir: "/tmp/openclaw-channel-plugin",
22+
channel: {
23+
id: params.channelId,
24+
configuredState: {
25+
env: {
26+
allOf: ["ALIAS_CHAT_TOKEN"],
27+
},
28+
},
29+
},
30+
};
31+
}
32+
33+
beforeEach(() => {
34+
listChannelCatalogEntriesMock.mockReset();
35+
});
36+
37+
describe("channel package-state probes", () => {
38+
it("uses channel ids when manifest plugin ids differ", () => {
39+
listChannelCatalogEntriesMock.mockReturnValue([
40+
makeBundledChannelCatalogEntry({
41+
pluginId: "vendor-alias-chat-plugin",
42+
channelId: "alias-chat",
43+
}),
44+
]);
45+
46+
expect(listBundledChannelIdsForPackageState("configuredState")).toEqual(["alias-chat"]);
47+
expect(
48+
hasBundledChannelPackageState({
49+
metadataKey: "configuredState",
50+
channelId: "alias-chat",
51+
cfg: {},
52+
env: { ALIAS_CHAT_TOKEN: "token" },
53+
}),
54+
).toBe(true);
55+
expect(
56+
hasBundledChannelPackageState({
57+
metadataKey: "configuredState",
58+
channelId: "vendor-alias-chat-plugin",
59+
cfg: {},
60+
env: { ALIAS_CHAT_TOKEN: "token" },
61+
}),
62+
).toBe(false);
63+
});
64+
});

src/channels/plugins/package-state-probes.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,16 @@ function resolveChannelPackageStateChecker(params: {
137137
}
138138
}
139139

140+
function resolvePackageStateChannelId(entry: PluginChannelCatalogEntry): string | undefined {
141+
return normalizeOptionalString(entry.channel.id);
142+
}
143+
140144
export function listBundledChannelIdsForPackageState(
141145
metadataKey: ChannelPackageStateMetadataKey,
142146
): string[] {
143-
return listChannelPackageStateCatalog(metadataKey).map((entry) => entry.pluginId);
147+
return listChannelPackageStateCatalog(metadataKey)
148+
.map((entry) => resolvePackageStateChannelId(entry))
149+
.filter((channelId): channelId is string => Boolean(channelId));
144150
}
145151

146152
export function hasBundledChannelPackageState(params: {
@@ -149,8 +155,9 @@ export function hasBundledChannelPackageState(params: {
149155
cfg: OpenClawConfig;
150156
env?: NodeJS.ProcessEnv;
151157
}): boolean {
158+
const requestedChannelId = normalizeOptionalString(params.channelId);
152159
const entry = listChannelPackageStateCatalog(params.metadataKey).find(
153-
(candidate) => candidate.pluginId === params.channelId,
160+
(candidate) => resolvePackageStateChannelId(candidate) === requestedChannelId,
154161
);
155162
if (!entry) {
156163
return false;

src/channels/plugins/read-only-command-defaults.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,43 @@ describe("resolveReadOnlyChannelCommandDefaults", () => {
6565
workspaceDir: "/workspace",
6666
});
6767
});
68+
69+
it("resolves command defaults for manifest channel aliases", () => {
70+
loadPluginMetadataSnapshot.mockReturnValue({
71+
index: {
72+
plugins: [
73+
{
74+
pluginId: "vendor-demo-plugin",
75+
origin: "global",
76+
enabled: true,
77+
enabledByDefault: true,
78+
},
79+
],
80+
},
81+
plugins: [
82+
{
83+
id: "vendor-demo-plugin",
84+
origin: "global",
85+
channels: ["demo"],
86+
channelConfigs: {
87+
demo: {
88+
commands: {
89+
nativeCommandsAutoEnabled: true,
90+
nativeSkillsAutoEnabled: false,
91+
},
92+
},
93+
},
94+
},
95+
],
96+
});
97+
98+
expect(
99+
resolveReadOnlyChannelCommandDefaults("demo", {
100+
config: {},
101+
}),
102+
).toEqual({
103+
nativeCommandsAutoEnabled: true,
104+
nativeSkillsAutoEnabled: false,
105+
});
106+
});
68107
});

src/channels/plugins/read-only-command-defaults.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,6 @@ export function resolveReadOnlyChannelCommandDefaults(
7474
if (!record.channels.includes(normalizedChannelId)) {
7575
continue;
7676
}
77-
if (
78-
record.id !== normalizedChannelId &&
79-
record.channelCatalogMeta?.id !== normalizedChannelId
80-
) {
81-
continue;
82-
}
8377
if (!isInstalledPluginEnabled(snapshot.index, record.id, options.config)) {
8478
continue;
8579
}
@@ -92,9 +86,11 @@ export function resolveReadOnlyChannelCommandDefaults(
9286
!Array.isArray(channelConfigValue)
9387
? (channelConfigValue as ManifestChannelConfigRecord)
9488
: undefined;
95-
const commands = normalizeChannelCommandDefaults(
96-
channelConfig?.commands ?? record.channelCatalogMeta?.commands,
97-
);
89+
const catalogCommands =
90+
record.channelCatalogMeta?.id === normalizedChannelId
91+
? record.channelCatalogMeta.commands
92+
: undefined;
93+
const commands = normalizeChannelCommandDefaults(channelConfig?.commands ?? catalogCommands);
9894
if (commands) {
9995
return commands;
10096
}

0 commit comments

Comments
 (0)