fix(channels): load third-party official channel packages

This commit is contained in:
Vincent Koc
2026-05-03 01:22:30 -07:00
parent 8af6add03b
commit 4781b46056
15 changed files with 138 additions and 9 deletions

View File

@@ -22,6 +22,8 @@ Docs: https://docs.openclaw.ai
- Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc.
- Microsoft Teams: persist sent-message markers across Gateway restarts so follow-up replies to recent bot messages keep resolving the original conversation instead of dropping out after restart, with marker TTLs preserved on best-effort recovery. (#75585) Thanks @amknight.
- Matrix: persist pending approval reaction targets across Gateway restarts so room approvers can still approve or deny outstanding prompts after OpenClaw comes back online. (#75586) Thanks @amknight.
- Channels/onboarding: map third-party official WeCom and Yuanbao catalog entries to their published plugin ids so npm installs pass expected-plugin validation. Thanks @vincentkoc.
- Plugin SDK: restore the Mattermost and Matrix compatibility subpaths used by the pinned Yuanbao channel package so external installs can module-load after npm install. Thanks @vincentkoc.
- CLI/plugins: keep `plugins enable` and `plugins disable` from creating unconfigured channel config sections, so channel plugins with required setup fields no longer fail validation during lifecycle probes. Thanks @vincentkoc.
- Agents/sessions: keep delayed `sessions_send` A2A replies alive after soft wait-window timeouts, while preserving terminal run timeouts and avoiding stale target replies in requester sessions. Fixes #76443. Thanks @ryswork1993 and @vincentkoc.
- CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc.

View File

@@ -695,6 +695,14 @@
"types": "./dist/plugin-sdk/discord.d.ts",
"default": "./dist/plugin-sdk/discord.js"
},
"./plugin-sdk/mattermost": {
"types": "./dist/plugin-sdk/mattermost.d.ts",
"default": "./dist/plugin-sdk/mattermost.js"
},
"./plugin-sdk/matrix": {
"types": "./dist/plugin-sdk/matrix.d.ts",
"default": "./dist/plugin-sdk/matrix.js"
},
"./plugin-sdk/device-bootstrap": {
"types": "./dist/plugin-sdk/device-bootstrap.d.ts",
"default": "./dist/plugin-sdk/device-bootstrap.js"

View File

@@ -6,6 +6,10 @@
"source": "external",
"kind": "channel",
"openclaw": {
"plugin": {
"id": "wecom-openclaw-plugin",
"label": "WeCom"
},
"channel": {
"id": "wecom",
"label": "WeCom",
@@ -30,6 +34,10 @@
"source": "external",
"kind": "channel",
"openclaw": {
"plugin": {
"id": "openclaw-plugin-yuanbao",
"label": "Yuanbao"
},
"channel": {
"id": "yuanbao",
"label": "Yuanbao",

View File

@@ -150,6 +150,8 @@
"direct-dm-access",
"direct-dm-guard-policy",
"discord",
"mattermost",
"matrix",
"device-bootstrap",
"diagnostic-runtime",
"error-runtime",

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { getChannelPluginCatalogEntry } from "./catalog.js";
describe("channel plugin catalog", () => {
it("keeps third-party official channel ids mapped to their published plugin ids", () => {
const options = {
workspaceDir: "/tmp/openclaw-channel-catalog-empty-workspace",
env: {},
};
expect(getChannelPluginCatalogEntry("wecom", options)).toEqual(
expect.objectContaining({
id: "wecom",
pluginId: "wecom-openclaw-plugin",
trustedSourceLinkedOfficialInstall: true,
install: expect.objectContaining({
npmSpec: "@wecom/wecom-openclaw-plugin@2026.4.23",
}),
}),
);
expect(getChannelPluginCatalogEntry("yuanbao", options)).toEqual(
expect.objectContaining({
id: "yuanbao",
pluginId: "openclaw-plugin-yuanbao",
trustedSourceLinkedOfficialInstall: true,
install: expect.objectContaining({
npmSpec: "openclaw-plugin-yuanbao@2.11.0",
}),
}),
);
});
});

View File

@@ -347,6 +347,7 @@ function buildExternalCatalogEntry(
): ChannelPluginCatalogEntry | null {
const manifest = entry[MANIFEST_KEY];
return buildCatalogEntryFromManifest({
pluginId: manifest?.plugin?.id,
packageName: entry.name,
trustedSourceLinkedOfficialInstall: options?.trustedSourceLinkedOfficialInstall,
channel: manifest?.channel,

View File

@@ -709,10 +709,12 @@ describe("plugins cli install", () => {
it("passes official external catalog integrity to npm installs", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("wecom");
const enabledCfg = createEnabledPluginConfig("wecom-openclaw-plugin");
loadConfig.mockReturnValue(cfg);
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("wecom"));
installPluginFromNpmSpec.mockResolvedValue(
createNpmPluginInstallResult("wecom-openclaw-plugin"),
);
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
@@ -724,7 +726,7 @@ describe("plugins cli install", () => {
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@wecom/wecom-openclaw-plugin@2026.4.23",
expectedPluginId: "wecom",
expectedPluginId: "wecom-openclaw-plugin",
expectedIntegrity:
"sha512-bnzfdIEEu1/LFvcdyjaTkyxt27w6c7dqhkPezU62OWaqmcdFsUGR3T55USK/O9pIKsNcnL1Tnu1pqKYCWHFgWQ==",
trustedSourceLinkedOfficialInstall: true,

View File

@@ -607,7 +607,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
// npm-only entry (no local path)
const npmOnlyEntry: ChannelPluginCatalogEntry = {
id: "wecom",
pluginId: "wecom",
pluginId: "wecom-openclaw-plugin",
meta: {
id: "wecom",
label: "WeCom",
@@ -621,8 +621,8 @@ describe("ensureChannelSetupPluginInstalled", () => {
};
installPluginFromNpmSpec.mockResolvedValue({
ok: true,
pluginId: "wecom",
installPath: "/tmp/wecom",
pluginId: "wecom-openclaw-plugin",
installPath: "/tmp/wecom-openclaw-plugin",
});
vi.mocked(fs.existsSync).mockReturnValue(false);
resolveBundledPluginSources.mockReturnValue(new Map());
@@ -637,7 +637,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
expect(select).not.toHaveBeenCalled();
expect(result.installed).toBe(true);
expect(result.pluginId).toBe("wecom");
expect(result.pluginId).toBe("wecom-openclaw-plugin");
});
it("reloads the setup plugin registry without using plugin registry cache", () => {

6
src/plugin-sdk/matrix.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* @deprecated Compatibility facade for older third-party channel packages that
* imported the previous Matrix-shaped helper bundle. New plugins should import
* `openclaw/plugin-sdk/run-command` directly.
*/
export { runPluginCommandWithTimeout } from "./run-command.js";

View File

@@ -0,0 +1,13 @@
/**
* @deprecated Compatibility facade for older third-party channel packages that
* imported the previous Mattermost-shaped helper bundle. New plugins should
* import the generic SDK subpaths directly.
*/
export { resolveControlCommandGate } from "./command-auth.js";
export { formatPairingApproveHint } from "./channel-plugin-common.js";
export type { HistoryEntry } from "./reply-history.js";
export {
buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled,
recordPendingHistoryEntryIfEnabled,
} from "./reply-history.js";

View File

@@ -541,6 +541,14 @@ describe("plugin-sdk subpath exports", () => {
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
]);
expectSourceMentions("mattermost", [
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"formatPairingApproveHint",
"recordPendingHistoryEntryIfEnabled",
"resolveControlCommandGate",
]);
expectSourceMentions("matrix", ["runPluginCommandWithTimeout"]);
expectSourceContract("reply-runtime", {
omits: [
"buildPendingHistoryContextFromMap",

View File

@@ -1714,6 +1714,10 @@ export type OpenClawPackageManifest = {
setupEntry?: string;
runtimeSetupEntry?: string;
setupFeatures?: OpenClawPackageSetupFeatures;
plugin?: {
id?: string;
label?: string;
};
channel?: PluginPackageChannel;
install?: PluginPackageInstall;
startup?: OpenClawPackageStartup;

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import {
getOfficialExternalPluginCatalogEntry,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "./official-external-plugin-catalog.js";
describe("official external plugin catalog", () => {
it("resolves third-party channel lookup aliases to published plugin ids", () => {
const wecomByChannel = getOfficialExternalPluginCatalogEntry("wecom");
const wecomByPlugin = getOfficialExternalPluginCatalogEntry("wecom-openclaw-plugin");
const yuanbaoByChannel = getOfficialExternalPluginCatalogEntry("yuanbao");
expect(resolveOfficialExternalPluginId(wecomByChannel!)).toBe("wecom-openclaw-plugin");
expect(resolveOfficialExternalPluginId(wecomByPlugin!)).toBe("wecom-openclaw-plugin");
expect(resolveOfficialExternalPluginInstall(wecomByChannel!)?.npmSpec).toBe(
"@wecom/wecom-openclaw-plugin@2026.4.23",
);
expect(resolveOfficialExternalPluginId(yuanbaoByChannel!)).toBe("openclaw-plugin-yuanbao");
expect(resolveOfficialExternalPluginInstall(yuanbaoByChannel!)?.npmSpec).toBe(
"openclaw-plugin-yuanbao@2.11.0",
);
});
});

View File

@@ -112,6 +112,17 @@ export function resolveOfficialExternalPluginId(
);
}
function resolveOfficialExternalPluginLookupIds(
entry: OfficialExternalPluginCatalogEntry,
): string[] {
const manifest = getOfficialExternalPluginCatalogManifest(entry);
return [
normalizeOptionalString(manifest?.plugin?.id),
normalizeOptionalString(manifest?.channel?.id),
normalizeOptionalString(manifest?.providers?.[0]?.id),
].filter((value, index, all): value is string => Boolean(value) && all.indexOf(value) === index);
}
export function resolveOfficialExternalPluginLabel(
entry: OfficialExternalPluginCatalogEntry,
): string {
@@ -183,7 +194,7 @@ export function getOfficialExternalPluginCatalogEntry(
if (!normalized) {
return undefined;
}
return listOfficialExternalPluginCatalogEntries().find(
(entry) => resolveOfficialExternalPluginId(entry) === normalized,
return listOfficialExternalPluginCatalogEntries().find((entry) =>
resolveOfficialExternalPluginLookupIds(entry).includes(normalized),
);
}

View File

@@ -74,6 +74,10 @@ describe("buildOfficialChannelCatalog", () => {
expect.objectContaining({
name: "@wecom/wecom-openclaw-plugin",
openclaw: expect.objectContaining({
plugin: {
id: "wecom-openclaw-plugin",
label: "WeCom",
},
channel: expect.objectContaining({
id: "wecom",
label: "WeCom",
@@ -89,6 +93,10 @@ describe("buildOfficialChannelCatalog", () => {
expect.objectContaining({
name: "openclaw-plugin-yuanbao",
openclaw: expect.objectContaining({
plugin: {
id: "openclaw-plugin-yuanbao",
label: "Yuanbao",
},
channel: expect.objectContaining({
id: "yuanbao",
label: "Yuanbao",