fix(plugins): share command registry across module graphs
This commit is contained in:
parent
009a10bce2
commit
e530563375
@ -108,6 +108,64 @@ describe("registerPluginCommand", () => {
|
||||
expect(getPluginCommandSpecs("slack")).toEqual([]);
|
||||
});
|
||||
|
||||
it("shares plugin commands across duplicated module instances", async () => {
|
||||
const duplicateCommands = (await import("./commands.js?duplicate=1")) as {
|
||||
clearPluginCommands: () => void;
|
||||
getPluginCommandSpecs: (provider?: string) => Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
acceptsArgs: boolean;
|
||||
}>;
|
||||
registerPluginCommand: typeof registerPluginCommand;
|
||||
};
|
||||
|
||||
clearPluginCommands();
|
||||
duplicateCommands.clearPluginCommands();
|
||||
|
||||
expect(
|
||||
duplicateCommands.registerPluginCommand("demo-plugin", {
|
||||
name: "phone",
|
||||
description: "Phone control",
|
||||
acceptsArgs: true,
|
||||
nativeNames: {
|
||||
telegram: "phone",
|
||||
},
|
||||
handler: async () => ({ text: "ok" }),
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
expect(getPluginCommandSpecs("telegram")).toEqual([
|
||||
{
|
||||
name: "phone",
|
||||
description: "Phone control",
|
||||
acceptsArgs: true,
|
||||
},
|
||||
]);
|
||||
|
||||
clearPluginCommands();
|
||||
expect(duplicateCommands.getPluginCommandSpecs("telegram")).toEqual([]);
|
||||
|
||||
expect(
|
||||
registerPluginCommand("demo-plugin", {
|
||||
name: "voice",
|
||||
description: "Voice control",
|
||||
acceptsArgs: false,
|
||||
nativeNames: {
|
||||
telegram: "voice",
|
||||
},
|
||||
handler: async () => ({ text: "ok" }),
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
expect(duplicateCommands.getPluginCommandSpecs("telegram")).toEqual([
|
||||
{
|
||||
name: "voice",
|
||||
description: "Voice control",
|
||||
acceptsArgs: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches provider-specific native aliases back to the canonical command", () => {
|
||||
const result = registerPluginCommand("demo-plugin", {
|
||||
name: "voice",
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import {
|
||||
detachPluginConversationBinding,
|
||||
getCurrentPluginConversationBinding,
|
||||
@ -25,11 +26,28 @@ type RegisteredPluginCommand = OpenClawPluginCommandDefinition & {
|
||||
pluginRoot?: string;
|
||||
};
|
||||
|
||||
// Registry of plugin commands
|
||||
const pluginCommands: Map<string, RegisteredPluginCommand> = new Map();
|
||||
type PluginCommandRegistryState = {
|
||||
pluginCommands: Map<string, RegisteredPluginCommand>;
|
||||
registryLocked: boolean;
|
||||
};
|
||||
|
||||
// Lock to prevent modifications during command execution
|
||||
let registryLocked = false;
|
||||
const PLUGIN_COMMAND_REGISTRY_STATE_KEY = Symbol.for("openclaw.plugins.command-registry-state");
|
||||
|
||||
/**
|
||||
* Keep plugin command state on globalThis so source-loaded plugin-sdk shims,
|
||||
* bundled dist extensions, and Jiti-transpiled copies all share one registry.
|
||||
* Without this, plugin registration can succeed in one module graph while
|
||||
* Telegram/Discord read commands from a different copy and see an empty set.
|
||||
*/
|
||||
const registryState = resolveGlobalSingleton<PluginCommandRegistryState>(
|
||||
PLUGIN_COMMAND_REGISTRY_STATE_KEY,
|
||||
() => ({
|
||||
pluginCommands: new Map<string, RegisteredPluginCommand>(),
|
||||
registryLocked: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const pluginCommands = registryState.pluginCommands;
|
||||
|
||||
// Maximum allowed length for command arguments (defense in depth)
|
||||
const MAX_ARGS_LENGTH = 4096;
|
||||
@ -172,7 +190,7 @@ export function registerPluginCommand(
|
||||
opts?: { pluginName?: string; pluginRoot?: string },
|
||||
): CommandRegistrationResult {
|
||||
// Prevent registration while commands are being processed
|
||||
if (registryLocked) {
|
||||
if (registryState.registryLocked) {
|
||||
return { ok: false, error: "Cannot register commands while processing is in progress" };
|
||||
}
|
||||
|
||||
@ -451,7 +469,7 @@ export async function executePluginCommand(params: {
|
||||
};
|
||||
|
||||
// Lock registry during execution to prevent concurrent modifications
|
||||
registryLocked = true;
|
||||
registryState.registryLocked = true;
|
||||
try {
|
||||
const result = await command.handler(ctx);
|
||||
logVerbose(
|
||||
@ -464,7 +482,7 @@ export async function executePluginCommand(params: {
|
||||
// Don't leak internal error details - return a safe generic message
|
||||
return { text: "⚠️ Command failed. Please try again later." };
|
||||
} finally {
|
||||
registryLocked = false;
|
||||
registryState.registryLocked = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user