fix(plugins): deduplicate registry records for same-id plugins from different paths
When the same plugin (e.g. feishu) is discovered from both the bundled extensions/ directory and the npm-installed ~/.openclaw/extensions/ directory, the manifest registry previously added both records. Having two records with the same plugin id caused duplicate channel registration and Gateway instability when modifying channel configuration. Apply the existing precedence logic (config > workspace > global > bundled) to genuine duplicates: keep the higher-precedence origin and skip the lower-precedence duplicate, just like the same-path dedup already does. The duplicate warning diagnostic is preserved so users are still informed. Closes #37028
This commit is contained in:
parent
adec8b28bb
commit
dd64501262
@ -150,7 +150,65 @@ describe("loadPluginManifestRegistry", () => {
|
||||
}),
|
||||
];
|
||||
|
||||
expect(countDuplicateWarnings(loadRegistry(candidates))).toBe(1);
|
||||
const registry = loadRegistry(candidates);
|
||||
expect(countDuplicateWarnings(registry)).toBe(1);
|
||||
// Only one record should be kept to prevent Gateway instability
|
||||
expect(registry.plugins.filter((p) => p.id === "test-plugin")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps higher-precedence origin when genuine duplicate is detected from different paths", () => {
|
||||
const bundledDir = makeTempDir();
|
||||
const globalDir = makeTempDir();
|
||||
const manifest = { id: "feishu-dup", configSchema: { type: "object" } };
|
||||
writeManifest(bundledDir, manifest);
|
||||
writeManifest(globalDir, manifest);
|
||||
|
||||
// Bundled discovered first, then global (npm-installed copy)
|
||||
const candidates: PluginCandidate[] = [
|
||||
createPluginCandidate({
|
||||
idHint: "feishu-dup",
|
||||
rootDir: bundledDir,
|
||||
origin: "bundled",
|
||||
}),
|
||||
createPluginCandidate({
|
||||
idHint: "feishu-dup",
|
||||
rootDir: globalDir,
|
||||
origin: "global",
|
||||
}),
|
||||
];
|
||||
|
||||
const registry = loadRegistry(candidates);
|
||||
expect(countDuplicateWarnings(registry)).toBe(1);
|
||||
expect(registry.plugins.filter((p) => p.id === "feishu-dup")).toHaveLength(1);
|
||||
// Global has higher precedence than bundled (config > workspace > global > bundled)
|
||||
expect(registry.plugins[0]?.origin).toBe("global");
|
||||
});
|
||||
|
||||
it("keeps existing record when genuine duplicate has lower precedence", () => {
|
||||
const configDir = makeTempDir();
|
||||
const globalDir = makeTempDir();
|
||||
const manifest = { id: "lower-prec", configSchema: { type: "object" } };
|
||||
writeManifest(configDir, manifest);
|
||||
writeManifest(globalDir, manifest);
|
||||
|
||||
const candidates: PluginCandidate[] = [
|
||||
createPluginCandidate({
|
||||
idHint: "lower-prec",
|
||||
rootDir: configDir,
|
||||
origin: "config",
|
||||
}),
|
||||
createPluginCandidate({
|
||||
idHint: "lower-prec",
|
||||
rootDir: globalDir,
|
||||
origin: "global",
|
||||
}),
|
||||
];
|
||||
|
||||
const registry = loadRegistry(candidates);
|
||||
expect(countDuplicateWarnings(registry)).toBe(1);
|
||||
expect(registry.plugins.filter((p) => p.id === "lower-prec")).toHaveLength(1);
|
||||
// Config has higher precedence, should be kept
|
||||
expect(registry.plugins[0]?.origin).toBe("config");
|
||||
});
|
||||
|
||||
it("suppresses duplicate warning when candidates share the same physical directory via symlink", () => {
|
||||
|
||||
@ -233,8 +233,22 @@ export function loadPluginManifestRegistry(params: {
|
||||
level: "warn",
|
||||
pluginId: manifest.id,
|
||||
source: candidate.source,
|
||||
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`,
|
||||
message: `duplicate plugin id detected; skipping duplicate from ${candidate.source}`,
|
||||
});
|
||||
// Genuine duplicate from a different physical path: apply the same
|
||||
// precedence logic as same-path duplicates to avoid registering two
|
||||
// records with the same plugin id (which causes Gateway instability).
|
||||
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
|
||||
records[existing.recordIndex] = buildRecord({
|
||||
manifest,
|
||||
candidate,
|
||||
manifestPath: manifestRes.manifestPath,
|
||||
schemaCacheKey,
|
||||
configSchema,
|
||||
});
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: records.length });
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user