diff --git a/CHANGELOG.md b/CHANGELOG.md index e417fa00610..85ca536816e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/package.json b/package.json index 5ca7e0ed74c..e152a29a564 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/lib/official-external-channel-catalog.json b/scripts/lib/official-external-channel-catalog.json index d51887c0f61..fbd530304fc 100644 --- a/scripts/lib/official-external-channel-catalog.json +++ b/scripts/lib/official-external-channel-catalog.json @@ -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", diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 3cff80ff52c..f8113513fa6 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -150,6 +150,8 @@ "direct-dm-access", "direct-dm-guard-policy", "discord", + "mattermost", + "matrix", "device-bootstrap", "diagnostic-runtime", "error-runtime", diff --git a/src/channels/plugins/catalog.test.ts b/src/channels/plugins/catalog.test.ts new file mode 100644 index 00000000000..d9c11823b07 --- /dev/null +++ b/src/channels/plugins/catalog.test.ts @@ -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", + }), + }), + ); + }); +}); diff --git a/src/channels/plugins/catalog.ts b/src/channels/plugins/catalog.ts index a9696acd21b..4ff379345a6 100644 --- a/src/channels/plugins/catalog.ts +++ b/src/channels/plugins/catalog.ts @@ -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, diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index ccb2fddf2fd..6c969d4a71a 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -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, diff --git a/src/commands/channel-setup/plugin-install.test.ts b/src/commands/channel-setup/plugin-install.test.ts index 0103ea230ec..00ae250acf5 100644 --- a/src/commands/channel-setup/plugin-install.test.ts +++ b/src/commands/channel-setup/plugin-install.test.ts @@ -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", () => { diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts new file mode 100644 index 00000000000..fdbfac52aaa --- /dev/null +++ b/src/plugin-sdk/matrix.ts @@ -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"; diff --git a/src/plugin-sdk/mattermost.ts b/src/plugin-sdk/mattermost.ts new file mode 100644 index 00000000000..31c0b62ff5c --- /dev/null +++ b/src/plugin-sdk/mattermost.ts @@ -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"; diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index 77f4b221aa4..299982ccedf 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -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", diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index ffa227113a5..cc7c05e1e8e 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -1714,6 +1714,10 @@ export type OpenClawPackageManifest = { setupEntry?: string; runtimeSetupEntry?: string; setupFeatures?: OpenClawPackageSetupFeatures; + plugin?: { + id?: string; + label?: string; + }; channel?: PluginPackageChannel; install?: PluginPackageInstall; startup?: OpenClawPackageStartup; diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts new file mode 100644 index 00000000000..b4ad7a301d5 --- /dev/null +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -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", + ); + }); +}); diff --git a/src/plugins/official-external-plugin-catalog.ts b/src/plugins/official-external-plugin-catalog.ts index fb91b6221a7..00aca019ed3 100644 --- a/src/plugins/official-external-plugin-catalog.ts +++ b/src/plugins/official-external-plugin-catalog.ts @@ -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), ); } diff --git a/test/official-channel-catalog.test.ts b/test/official-channel-catalog.test.ts index ceaeacc742a..945d3bacdd5 100644 --- a/test/official-channel-catalog.test.ts +++ b/test/official-channel-catalog.test.ts @@ -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",