mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 12:28:37 +00:00
refactor: stage plugin enable-state resolution
This commit is contained in:
@@ -5,14 +5,37 @@ import {
|
|||||||
resolveEnableState,
|
resolveEnableState,
|
||||||
} from "./config-state.js";
|
} from "./config-state.js";
|
||||||
|
|
||||||
|
function normalizedPlugins(config: Parameters<typeof normalizePluginsConfig>[0]) {
|
||||||
|
return normalizePluginsConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBundledState(id: string, config: Parameters<typeof normalizePluginsConfig>[0]) {
|
||||||
|
return resolveEnableState(id, "bundled", normalizedPlugins(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBundledEffectiveState(config: Parameters<typeof normalizePluginsConfig>[0]) {
|
||||||
|
return resolveEffectiveEnableState({
|
||||||
|
id: "telegram",
|
||||||
|
origin: "bundled",
|
||||||
|
config: normalizedPlugins(config),
|
||||||
|
rootConfig: {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("normalizePluginsConfig", () => {
|
describe("normalizePluginsConfig", () => {
|
||||||
it("uses default memory slot when not specified", () => {
|
it("uses default memory slot when not specified", () => {
|
||||||
const result = normalizePluginsConfig({});
|
const result = normalizedPlugins({});
|
||||||
expect(result.slots.memory).toBe("memory-core");
|
expect(result.slots.memory).toBe("memory-core");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects explicit memory slot value", () => {
|
it("respects explicit memory slot value", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
slots: { memory: "custom-memory" },
|
slots: { memory: "custom-memory" },
|
||||||
});
|
});
|
||||||
expect(result.slots.memory).toBe("custom-memory");
|
expect(result.slots.memory).toBe("custom-memory");
|
||||||
@@ -20,40 +43,40 @@ describe("normalizePluginsConfig", () => {
|
|||||||
|
|
||||||
it("disables memory slot when set to 'none' (case insensitive)", () => {
|
it("disables memory slot when set to 'none' (case insensitive)", () => {
|
||||||
expect(
|
expect(
|
||||||
normalizePluginsConfig({
|
normalizedPlugins({
|
||||||
slots: { memory: "none" },
|
slots: { memory: "none" },
|
||||||
}).slots.memory,
|
}).slots.memory,
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
normalizePluginsConfig({
|
normalizedPlugins({
|
||||||
slots: { memory: "None" },
|
slots: { memory: "None" },
|
||||||
}).slots.memory,
|
}).slots.memory,
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("trims whitespace from memory slot value", () => {
|
it("trims whitespace from memory slot value", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
slots: { memory: " custom-memory " },
|
slots: { memory: " custom-memory " },
|
||||||
});
|
});
|
||||||
expect(result.slots.memory).toBe("custom-memory");
|
expect(result.slots.memory).toBe("custom-memory");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses default when memory slot is empty string", () => {
|
it("uses default when memory slot is empty string", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
slots: { memory: "" },
|
slots: { memory: "" },
|
||||||
});
|
});
|
||||||
expect(result.slots.memory).toBe("memory-core");
|
expect(result.slots.memory).toBe("memory-core");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses default when memory slot is whitespace only", () => {
|
it("uses default when memory slot is whitespace only", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
slots: { memory: " " },
|
slots: { memory: " " },
|
||||||
});
|
});
|
||||||
expect(result.slots.memory).toBe("memory-core");
|
expect(result.slots.memory).toBe("memory-core");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes plugin hook policy flags", () => {
|
it("normalizes plugin hook policy flags", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
entries: {
|
entries: {
|
||||||
"voice-call": {
|
"voice-call": {
|
||||||
hooks: {
|
hooks: {
|
||||||
@@ -66,7 +89,7 @@ describe("normalizePluginsConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("drops invalid plugin hook policy values", () => {
|
it("drops invalid plugin hook policy values", () => {
|
||||||
const result = normalizePluginsConfig({
|
const result = normalizedPlugins({
|
||||||
entries: {
|
entries: {
|
||||||
"voice-call": {
|
"voice-call": {
|
||||||
hooks: {
|
hooks: {
|
||||||
@@ -80,31 +103,15 @@ describe("normalizePluginsConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveEffectiveEnableState", () => {
|
describe("resolveEffectiveEnableState", () => {
|
||||||
function resolveBundledTelegramState(config: Parameters<typeof normalizePluginsConfig>[0]) {
|
|
||||||
const normalized = normalizePluginsConfig(config);
|
|
||||||
return resolveEffectiveEnableState({
|
|
||||||
id: "telegram",
|
|
||||||
origin: "bundled",
|
|
||||||
config: normalized,
|
|
||||||
rootConfig: {
|
|
||||||
channels: {
|
|
||||||
telegram: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it("enables bundled channels when channels.<id>.enabled=true", () => {
|
it("enables bundled channels when channels.<id>.enabled=true", () => {
|
||||||
const state = resolveBundledTelegramState({
|
const state = resolveBundledEffectiveState({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
expect(state).toEqual({ enabled: true });
|
expect(state).toEqual({ enabled: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps explicit plugin-level disable authoritative", () => {
|
it("keeps explicit plugin-level disable authoritative", () => {
|
||||||
const state = resolveBundledTelegramState({
|
const state = resolveBundledEffectiveState({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
entries: {
|
entries: {
|
||||||
telegram: {
|
telegram: {
|
||||||
@@ -114,35 +121,35 @@ describe("resolveEffectiveEnableState", () => {
|
|||||||
});
|
});
|
||||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not let channel enablement bypass allowlist misses", () => {
|
||||||
|
const state = resolveBundledEffectiveState({
|
||||||
|
enabled: true,
|
||||||
|
allow: ["discord"],
|
||||||
|
});
|
||||||
|
expect(state).toEqual({ enabled: false, reason: "not in allowlist" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveEnableState", () => {
|
describe("resolveEnableState", () => {
|
||||||
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
|
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
|
||||||
const state = resolveEnableState(
|
const state = resolveBundledState("memory-core", {
|
||||||
"memory-core",
|
allow: ["telegram"],
|
||||||
"bundled",
|
slots: { memory: "memory-core" },
|
||||||
normalizePluginsConfig({
|
});
|
||||||
allow: ["telegram"],
|
|
||||||
slots: { memory: "memory-core" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(state).toEqual({ enabled: true });
|
expect(state).toEqual({ enabled: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps explicit disable authoritative for the selected memory slot plugin", () => {
|
it("keeps explicit disable authoritative for the selected memory slot plugin", () => {
|
||||||
const state = resolveEnableState(
|
const state = resolveBundledState("memory-core", {
|
||||||
"memory-core",
|
allow: ["telegram"],
|
||||||
"bundled",
|
slots: { memory: "memory-core" },
|
||||||
normalizePluginsConfig({
|
entries: {
|
||||||
allow: ["telegram"],
|
"memory-core": {
|
||||||
slots: { memory: "memory-core" },
|
enabled: false,
|
||||||
entries: {
|
|
||||||
"memory-core": {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
);
|
});
|
||||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,37 +186,123 @@ export function isTestDefaultMemorySlotDisabled(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnableStateCode =
|
||||||
|
| "plugins_disabled"
|
||||||
|
| "blocked_by_denylist"
|
||||||
|
| "disabled_in_config"
|
||||||
|
| "selected_memory_slot"
|
||||||
|
| "not_in_allowlist"
|
||||||
|
| "enabled_in_config"
|
||||||
|
| "bundled_enabled_by_default"
|
||||||
|
| "bundled_disabled_by_default"
|
||||||
|
| "enabled";
|
||||||
|
|
||||||
|
type EnableStateDecision = {
|
||||||
|
enabled: boolean;
|
||||||
|
code: EnableStateCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENABLE_STATE_REASON_BY_CODE: Partial<Record<EnableStateCode, string>> = {
|
||||||
|
plugins_disabled: "plugins disabled",
|
||||||
|
blocked_by_denylist: "blocked by denylist",
|
||||||
|
disabled_in_config: "disabled in config",
|
||||||
|
not_in_allowlist: "not in allowlist",
|
||||||
|
bundled_disabled_by_default: "bundled (disabled by default)",
|
||||||
|
};
|
||||||
|
|
||||||
|
function finalizeEnableState(decision: EnableStateDecision): { enabled: boolean; reason?: string } {
|
||||||
|
return {
|
||||||
|
enabled: decision.enabled,
|
||||||
|
reason: ENABLE_STATE_REASON_BY_CODE[decision.code],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExplicitEnableStateDecision(params: {
|
||||||
|
id: string;
|
||||||
|
config: NormalizedPluginsConfig;
|
||||||
|
}): EnableStateDecision | undefined {
|
||||||
|
if (!params.config.enabled) {
|
||||||
|
return { enabled: false, code: "plugins_disabled" };
|
||||||
|
}
|
||||||
|
if (params.config.deny.includes(params.id)) {
|
||||||
|
return { enabled: false, code: "blocked_by_denylist" };
|
||||||
|
}
|
||||||
|
if (params.config.entries[params.id]?.enabled === false) {
|
||||||
|
return { enabled: false, code: "disabled_in_config" };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSlotEnableStateDecision(params: {
|
||||||
|
id: string;
|
||||||
|
config: NormalizedPluginsConfig;
|
||||||
|
}): EnableStateDecision | undefined {
|
||||||
|
if (params.config.slots.memory === params.id) {
|
||||||
|
return { enabled: true, code: "selected_memory_slot" };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAllowlistEnableStateDecision(params: {
|
||||||
|
id: string;
|
||||||
|
config: NormalizedPluginsConfig;
|
||||||
|
}): EnableStateDecision | undefined {
|
||||||
|
if (params.config.allow.length > 0 && !params.config.allow.includes(params.id)) {
|
||||||
|
return { enabled: false, code: "not_in_allowlist" };
|
||||||
|
}
|
||||||
|
if (params.config.entries[params.id]?.enabled === true) {
|
||||||
|
return { enabled: true, code: "enabled_in_config" };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBundledDefaultEnableStateDecision(
|
||||||
|
id: string,
|
||||||
|
origin: PluginRecord["origin"],
|
||||||
|
): EnableStateDecision {
|
||||||
|
if (origin === "bundled" && BUNDLED_ENABLED_BY_DEFAULT.has(id)) {
|
||||||
|
return { enabled: true, code: "bundled_enabled_by_default" };
|
||||||
|
}
|
||||||
|
if (origin === "bundled") {
|
||||||
|
return { enabled: false, code: "bundled_disabled_by_default" };
|
||||||
|
}
|
||||||
|
return { enabled: true, code: "enabled" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEnableStateDecision(
|
||||||
|
id: string,
|
||||||
|
origin: PluginRecord["origin"],
|
||||||
|
config: NormalizedPluginsConfig,
|
||||||
|
): EnableStateDecision {
|
||||||
|
return (
|
||||||
|
resolveExplicitEnableStateDecision({ id, config }) ??
|
||||||
|
resolveSlotEnableStateDecision({ id, config }) ??
|
||||||
|
resolveAllowlistEnableStateDecision({ id, config }) ??
|
||||||
|
resolveBundledDefaultEnableStateDecision(id, origin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBundledChannelOverride(params: {
|
||||||
|
id: string;
|
||||||
|
rootConfig?: OpenClawConfig;
|
||||||
|
decision: EnableStateDecision;
|
||||||
|
}): EnableStateDecision {
|
||||||
|
if (
|
||||||
|
!params.decision.enabled &&
|
||||||
|
params.decision.code === "bundled_disabled_by_default" &&
|
||||||
|
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
|
||||||
|
) {
|
||||||
|
return { enabled: true, code: "enabled" };
|
||||||
|
}
|
||||||
|
return params.decision;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveEnableState(
|
export function resolveEnableState(
|
||||||
id: string,
|
id: string,
|
||||||
origin: PluginRecord["origin"],
|
origin: PluginRecord["origin"],
|
||||||
config: NormalizedPluginsConfig,
|
config: NormalizedPluginsConfig,
|
||||||
): { enabled: boolean; reason?: string } {
|
): { enabled: boolean; reason?: string } {
|
||||||
if (!config.enabled) {
|
return finalizeEnableState(resolveEnableStateDecision(id, origin, config));
|
||||||
return { enabled: false, reason: "plugins disabled" };
|
|
||||||
}
|
|
||||||
if (config.deny.includes(id)) {
|
|
||||||
return { enabled: false, reason: "blocked by denylist" };
|
|
||||||
}
|
|
||||||
const entry = config.entries[id];
|
|
||||||
if (entry?.enabled === false) {
|
|
||||||
return { enabled: false, reason: "disabled in config" };
|
|
||||||
}
|
|
||||||
if (config.slots.memory === id) {
|
|
||||||
return { enabled: true };
|
|
||||||
}
|
|
||||||
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
|
||||||
return { enabled: false, reason: "not in allowlist" };
|
|
||||||
}
|
|
||||||
if (entry?.enabled === true) {
|
|
||||||
return { enabled: true };
|
|
||||||
}
|
|
||||||
if (origin === "bundled" && BUNDLED_ENABLED_BY_DEFAULT.has(id)) {
|
|
||||||
return { enabled: true };
|
|
||||||
}
|
|
||||||
if (origin === "bundled") {
|
|
||||||
return { enabled: false, reason: "bundled (disabled by default)" };
|
|
||||||
}
|
|
||||||
return { enabled: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBundledChannelEnabledByChannelConfig(
|
export function isBundledChannelEnabledByChannelConfig(
|
||||||
@@ -244,15 +330,13 @@ export function resolveEffectiveEnableState(params: {
|
|||||||
config: NormalizedPluginsConfig;
|
config: NormalizedPluginsConfig;
|
||||||
rootConfig?: OpenClawConfig;
|
rootConfig?: OpenClawConfig;
|
||||||
}): { enabled: boolean; reason?: string } {
|
}): { enabled: boolean; reason?: string } {
|
||||||
const base = resolveEnableState(params.id, params.origin, params.config);
|
return finalizeEnableState(
|
||||||
if (
|
applyBundledChannelOverride({
|
||||||
!base.enabled &&
|
id: params.id,
|
||||||
base.reason === "bundled (disabled by default)" &&
|
rootConfig: params.rootConfig,
|
||||||
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
|
decision: resolveEnableStateDecision(params.id, params.origin, params.config),
|
||||||
) {
|
}),
|
||||||
return { enabled: true };
|
);
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMemorySlotDecision(params: {
|
export function resolveMemorySlotDecision(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user