Compare commits
2 Commits
main
...
fix/plugin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d31c59fedc | ||
|
|
e530563375 |
@ -108,6 +108,65 @@ describe("registerPluginCommand", () => {
|
|||||||
expect(getPluginCommandSpecs("slack")).toEqual([]);
|
expect(getPluginCommandSpecs("slack")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shares plugin commands across duplicated module instances", async () => {
|
||||||
|
const duplicateSpecifier = "./commands.js?duplicate=1";
|
||||||
|
const duplicateCommands = (await import(duplicateSpecifier)) 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", () => {
|
it("matches provider-specific native aliases back to the canonical command", () => {
|
||||||
const result = registerPluginCommand("demo-plugin", {
|
const result = registerPluginCommand("demo-plugin", {
|
||||||
name: "voice",
|
name: "voice",
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js";
|
import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { logVerbose } from "../globals.js";
|
import { logVerbose } from "../globals.js";
|
||||||
|
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||||
import {
|
import {
|
||||||
detachPluginConversationBinding,
|
detachPluginConversationBinding,
|
||||||
getCurrentPluginConversationBinding,
|
getCurrentPluginConversationBinding,
|
||||||
@ -25,11 +26,28 @@ type RegisteredPluginCommand = OpenClawPluginCommandDefinition & {
|
|||||||
pluginRoot?: string;
|
pluginRoot?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registry of plugin commands
|
type PluginCommandRegistryState = {
|
||||||
const pluginCommands: Map<string, RegisteredPluginCommand> = new Map();
|
pluginCommands: Map<string, RegisteredPluginCommand>;
|
||||||
|
registryLocked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// Lock to prevent modifications during command execution
|
const PLUGIN_COMMAND_REGISTRY_STATE_KEY = Symbol.for("openclaw.plugins.command-registry-state");
|
||||||
let registryLocked = false;
|
|
||||||
|
/**
|
||||||
|
* 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)
|
// Maximum allowed length for command arguments (defense in depth)
|
||||||
const MAX_ARGS_LENGTH = 4096;
|
const MAX_ARGS_LENGTH = 4096;
|
||||||
@ -172,7 +190,7 @@ export function registerPluginCommand(
|
|||||||
opts?: { pluginName?: string; pluginRoot?: string },
|
opts?: { pluginName?: string; pluginRoot?: string },
|
||||||
): CommandRegistrationResult {
|
): CommandRegistrationResult {
|
||||||
// Prevent registration while commands are being processed
|
// Prevent registration while commands are being processed
|
||||||
if (registryLocked) {
|
if (registryState.registryLocked) {
|
||||||
return { ok: false, error: "Cannot register commands while processing is in progress" };
|
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
|
// Lock registry during execution to prevent concurrent modifications
|
||||||
registryLocked = true;
|
registryState.registryLocked = true;
|
||||||
try {
|
try {
|
||||||
const result = await command.handler(ctx);
|
const result = await command.handler(ctx);
|
||||||
logVerbose(
|
logVerbose(
|
||||||
@ -464,7 +482,7 @@ export async function executePluginCommand(params: {
|
|||||||
// Don't leak internal error details - return a safe generic message
|
// Don't leak internal error details - return a safe generic message
|
||||||
return { text: "⚠️ Command failed. Please try again later." };
|
return { text: "⚠️ Command failed. Please try again later." };
|
||||||
} finally {
|
} finally {
|
||||||
registryLocked = false;
|
registryState.registryLocked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user