mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(channels): load third-party official channel packages
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -150,6 +150,8 @@
|
||||
"direct-dm-access",
|
||||
"direct-dm-guard-policy",
|
||||
"discord",
|
||||
"mattermost",
|
||||
"matrix",
|
||||
"device-bootstrap",
|
||||
"diagnostic-runtime",
|
||||
"error-runtime",
|
||||
|
||||
32
src/channels/plugins/catalog.test.ts
Normal file
32
src/channels/plugins/catalog.test.ts
Normal 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",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
6
src/plugin-sdk/matrix.ts
Normal 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";
|
||||
13
src/plugin-sdk/mattermost.ts
Normal file
13
src/plugin-sdk/mattermost.ts
Normal 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";
|
||||
@@ -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",
|
||||
|
||||
@@ -1714,6 +1714,10 @@ export type OpenClawPackageManifest = {
|
||||
setupEntry?: string;
|
||||
runtimeSetupEntry?: string;
|
||||
setupFeatures?: OpenClawPackageSetupFeatures;
|
||||
plugin?: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
};
|
||||
channel?: PluginPackageChannel;
|
||||
install?: PluginPackageInstall;
|
||||
startup?: OpenClawPackageStartup;
|
||||
|
||||
24
src/plugins/official-external-plugin-catalog.test.ts
Normal file
24
src/plugins/official-external-plugin-catalog.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user