diff --git a/CHANGELOG.md b/CHANGELOG.md index dc26802bb6a..630fdf397bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Plugins/discovery: follow symlinked plugin directories in global and workspace plugin roots while keeping broken links ignored and existing package safety checks in place. Fixes #36754; carries forward #72695 and #63206. Thanks @Quackstro, @ming1523, and @xsfX20. - Plugins/install: allow exact package-manager peer links back to the trusted OpenClaw host package during install security scans while continuing to block spoofed or nested escaping `node_modules` symlinks. Carries forward #70819. Thanks @fgabelmannjr. - Plugins/install: resolve plugin install destinations from the active profile state dir across CLI, ClawHub, marketplace, local path, and channel setup installs, so `openclaw --profile plugins install ...` no longer writes into the default profile. Fixes #69960; carries forward #69971. Thanks @FrancisLyman and @Sanjays2402. +- Plugins/registry: suppress duplicate-plugin startup warnings when a tracked npm-installed plugin intentionally overrides the bundled plugin with the same id. Carries forward #48673. Thanks @abdushsk. - Plugins/startup: reuse canonical realpath lookups throughout each plugin discovery pass, including package and manifest boundary checks, so Windows npm-global startups no longer repeat expensive path resolution for the same plugin roots. Fixes #65733. Thanks @welfo-beo. - Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111. - Discord: persist routed model-picker overrides when the hidden `/model` dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129. diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index 9df35afc520..66ddc004b31 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -370,7 +370,7 @@ describe("loadPluginManifestRegistry", () => { expect(warning?.message).toContain(path.join(configDir, "index.ts")); }); - it("reports explicit installed globals as the effective duplicate winner", () => { + it("suppresses duplicate warnings for explicit installed globals overriding bundled plugins", () => { const bundledDir = makeTempDir(); const globalDir = makeTempDir(); const manifest = { id: "zalouser", configSchema: { type: "object" } }; @@ -399,11 +399,41 @@ describe("loadPluginManifestRegistry", () => { ], }); - expect( - registry.diagnostics.some((diag) => - diag.message.includes("bundled plugin will be overridden by global plugin"), - ), - ).toBe(true); + expect(countDuplicateWarnings(registry)).toBe(0); + expect(registry.plugins).toHaveLength(1); + expect(registry.plugins[0]?.origin).toBe("global"); + }); + + it("suppresses duplicate warnings when the installed global is discovered before bundled", () => { + const bundledDir = makeTempDir(); + const globalDir = makeTempDir(); + const manifest = { id: "zalouser", configSchema: { type: "object" } }; + writeManifest(bundledDir, manifest); + writeManifest(globalDir, manifest); + + const registry = loadPluginManifestRegistry({ + cache: false, + installRecords: { + zalouser: { + source: "npm", + installPath: globalDir, + }, + }, + candidates: [ + createPluginCandidate({ + idHint: "zalouser", + rootDir: globalDir, + origin: "global", + }), + createPluginCandidate({ + idHint: "zalouser", + rootDir: bundledDir, + origin: "bundled", + }), + ], + }); + + expect(countDuplicateWarnings(registry)).toBe(0); expect(registry.plugins).toHaveLength(1); expect(registry.plugins[0]?.origin).toBe("global"); }); diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 05eea882aa5..5a3f2ae785b 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -558,6 +558,34 @@ function resolveDuplicatePrecedenceRank(params: { return 4; } +function isIntentionalInstalledBundledDuplicate(params: { + pluginId: string; + left: PluginCandidate; + right: PluginCandidate; + config?: OpenClawConfig; + env: NodeJS.ProcessEnv; + installRecords: Record; +}): boolean { + const leftIsInstalled = matchesInstalledPluginRecord({ + pluginId: params.pluginId, + candidate: params.left, + config: params.config, + env: params.env, + installRecords: params.installRecords, + }); + const rightIsInstalled = matchesInstalledPluginRecord({ + pluginId: params.pluginId, + candidate: params.right, + config: params.config, + env: params.env, + installRecords: params.installRecords, + }); + return ( + (leftIsInstalled && params.right.origin === "bundled") || + (rightIsInstalled && params.left.origin === "bundled") + ); +} + export function loadPluginManifestRegistry( params: { config?: OpenClawConfig; @@ -740,6 +768,18 @@ export function loadPluginManifestRegistry( seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); pushManifestCompatibilityDiagnostics({ record, diagnostics }); } + if ( + isIntentionalInstalledBundledDuplicate({ + pluginId: manifest.id, + left: candidate, + right: existing.candidate, + config, + env, + installRecords: getInstallRecords(), + }) + ) { + continue; + } diagnostics.push({ level: "warn", pluginId: manifest.id,