mirror of https://github.com/openclaw/openclaw.git
Status: scope JSON plugin preload to configured channels
This commit is contained in:
parent
d8b927ee6a
commit
986b772a89
|
|
@ -7,19 +7,19 @@ import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
|||
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
||||
const CHANNEL_ENV_PREFIXES = [
|
||||
"BLUEBUBBLES_",
|
||||
"DISCORD_",
|
||||
"GOOGLECHAT_",
|
||||
"IRC_",
|
||||
"LINE_",
|
||||
"MATRIX_",
|
||||
"MSTEAMS_",
|
||||
"SIGNAL_",
|
||||
"SLACK_",
|
||||
"TELEGRAM_",
|
||||
"WHATSAPP_",
|
||||
"ZALOUSER_",
|
||||
"ZALO_",
|
||||
["BLUEBUBBLES_", "bluebubbles"],
|
||||
["DISCORD_", "discord"],
|
||||
["GOOGLECHAT_", "googlechat"],
|
||||
["IRC_", "irc"],
|
||||
["LINE_", "line"],
|
||||
["MATRIX_", "matrix"],
|
||||
["MSTEAMS_", "msteams"],
|
||||
["SIGNAL_", "signal"],
|
||||
["SLACK_", "slack"],
|
||||
["TELEGRAM_", "telegram"],
|
||||
["WHATSAPP_", "whatsapp"],
|
||||
["ZALOUSER_", "zalouser"],
|
||||
["ZALO_", "zalo"],
|
||||
] as const;
|
||||
|
||||
function hasNonEmptyString(value: unknown): boolean {
|
||||
|
|
@ -60,13 +60,49 @@ function hasWhatsAppAuthState(env: NodeJS.ProcessEnv): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export function listPotentialConfiguredChannelIds(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const configuredChannelIds = new Set<string>();
|
||||
const channels = isRecord(cfg.channels) ? cfg.channels : null;
|
||||
if (channels) {
|
||||
for (const [key, value] of Object.entries(channels)) {
|
||||
if (IGNORED_CHANNEL_CONFIG_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
if (recordHasKeys(value)) {
|
||||
configuredChannelIds.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (!hasNonEmptyString(value)) {
|
||||
continue;
|
||||
}
|
||||
for (const [prefix, channelId] of CHANNEL_ENV_PREFIXES) {
|
||||
if (key.startsWith(prefix)) {
|
||||
configuredChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
if (key === "TELEGRAM_BOT_TOKEN") {
|
||||
configuredChannelIds.add("telegram");
|
||||
}
|
||||
}
|
||||
if (hasWhatsAppAuthState(env)) {
|
||||
configuredChannelIds.add("whatsapp");
|
||||
}
|
||||
return [...configuredChannelIds];
|
||||
}
|
||||
|
||||
function hasEnvConfiguredChannel(env: NodeJS.ProcessEnv): boolean {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (!hasNonEmptyString(value)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
CHANNEL_ENV_PREFIXES.some((prefix) => key.startsWith(prefix)) ||
|
||||
CHANNEL_ENV_PREFIXES.some(([prefix]) => key.startsWith(prefix)) ||
|
||||
key === "TELEGRAM_BOT_TOKEN"
|
||||
) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
|
||||
resolveDefaultAgentId: vi.fn(() => "main"),
|
||||
loadConfig: vi.fn(),
|
||||
loadOpenClawPlugins: vi.fn(),
|
||||
loadPluginManifestRegistry: vi.fn(),
|
||||
getActivePluginRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: mocks.loadConfig,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: mocks.loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: mocks.getActivePluginRegistry,
|
||||
}));
|
||||
|
||||
describe("ensurePluginRegistryLoaded", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
mocks.loadConfig.mockReturnValue({
|
||||
plugins: { enabled: true },
|
||||
channels: { telegram: { enabled: false } },
|
||||
});
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "telegram", channels: ["telegram"] },
|
||||
{ id: "slack", channels: ["slack"] },
|
||||
{ id: "openai", channels: [] },
|
||||
],
|
||||
});
|
||||
mocks.getActivePluginRegistry.mockReturnValue({
|
||||
plugins: [],
|
||||
channels: [],
|
||||
tools: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("loads only configured channel plugins for configured-channels scope", async () => {
|
||||
const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js");
|
||||
|
||||
ensurePluginRegistryLoaded({ scope: "configured-channels" });
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["telegram"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reloads when escalating from configured-channels to channels", async () => {
|
||||
mocks.getActivePluginRegistry
|
||||
.mockReturnValueOnce({
|
||||
plugins: [],
|
||||
channels: [],
|
||||
tools: [],
|
||||
})
|
||||
.mockReturnValue({
|
||||
plugins: [{ id: "telegram" }],
|
||||
channels: [{ plugin: { id: "telegram" } }],
|
||||
tools: [],
|
||||
});
|
||||
|
||||
const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js");
|
||||
|
||||
ensurePluginRegistryLoaded({ scope: "configured-channels" });
|
||||
ensurePluginRegistryLoaded({ scope: "channels" });
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ onlyPluginIds: ["telegram"] }),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ onlyPluginIds: ["telegram", "slack"] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
|
|
@ -7,9 +8,22 @@ import { getActivePluginRegistry } from "../plugins/runtime.js";
|
|||
import type { PluginLogger } from "../plugins/types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
let pluginRegistryLoaded: "none" | "channels" | "all" = "none";
|
||||
let pluginRegistryLoaded: "none" | "configured-channels" | "channels" | "all" = "none";
|
||||
|
||||
export type PluginRegistryScope = "channels" | "all";
|
||||
export type PluginRegistryScope = "configured-channels" | "channels" | "all";
|
||||
|
||||
function scopeRank(scope: typeof pluginRegistryLoaded): number {
|
||||
switch (scope) {
|
||||
case "none":
|
||||
return 0;
|
||||
case "configured-channels":
|
||||
return 1;
|
||||
case "channels":
|
||||
return 2;
|
||||
case "all":
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChannelPluginIds(params: {
|
||||
config: ReturnType<typeof loadConfig>;
|
||||
|
|
@ -25,15 +39,30 @@ function resolveChannelPluginIds(params: {
|
|||
.map((plugin) => plugin.id);
|
||||
}
|
||||
|
||||
function resolveConfiguredChannelPluginIds(params: {
|
||||
config: ReturnType<typeof loadConfig>;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] {
|
||||
const configuredChannelIds = new Set(
|
||||
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
|
||||
);
|
||||
if (configuredChannelIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
return resolveChannelPluginIds(params).filter((pluginId) => configuredChannelIds.has(pluginId));
|
||||
}
|
||||
|
||||
export function ensurePluginRegistryLoaded(options?: { scope?: PluginRegistryScope }): void {
|
||||
const scope = options?.scope ?? "all";
|
||||
if (pluginRegistryLoaded === "all" || pluginRegistryLoaded === scope) {
|
||||
if (scopeRank(pluginRegistryLoaded) >= scopeRank(scope)) {
|
||||
return;
|
||||
}
|
||||
const active = getActivePluginRegistry();
|
||||
// Tests (and callers) can pre-seed a registry (e.g. `test/setup.ts`); avoid
|
||||
// doing an expensive load when we already have plugins/channels/tools.
|
||||
if (
|
||||
pluginRegistryLoaded === "none" &&
|
||||
active &&
|
||||
(active.plugins.length > 0 || active.channels.length > 0 || active.tools.length > 0)
|
||||
) {
|
||||
|
|
@ -52,15 +81,23 @@ export function ensurePluginRegistryLoaded(options?: { scope?: PluginRegistrySco
|
|||
config,
|
||||
workspaceDir,
|
||||
logger,
|
||||
...(scope === "channels"
|
||||
...(scope === "configured-channels"
|
||||
? {
|
||||
onlyPluginIds: resolveChannelPluginIds({
|
||||
onlyPluginIds: resolveConfiguredChannelPluginIds({
|
||||
config,
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
: scope === "channels"
|
||||
? {
|
||||
onlyPluginIds: resolveChannelPluginIds({
|
||||
config,
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
pluginRegistryLoaded = scope;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ describe("scanStatus", () => {
|
|||
expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preloads channel plugins for status --json when channel config exists", async () => {
|
||||
it("preloads configured channel plugins for status --json when channel config exists", async () => {
|
||||
mocks.readBestEffortConfig.mockResolvedValue({
|
||||
session: {},
|
||||
plugins: { enabled: false },
|
||||
|
|
@ -245,7 +245,9 @@ describe("scanStatus", () => {
|
|||
|
||||
await scanStatus({ json: true }, {} as never);
|
||||
|
||||
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({ scope: "channels" });
|
||||
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
scope: "configured-channels",
|
||||
});
|
||||
expect(mocks.probeGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ detailLevel: "presence" }),
|
||||
);
|
||||
|
|
@ -254,7 +256,7 @@ describe("scanStatus", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("preloads channel plugins for status --json when channel auth is env-only", async () => {
|
||||
it("preloads configured channel plugins for status --json when channel auth is env-only", async () => {
|
||||
const prevMatrixToken = process.env.MATRIX_ACCESS_TOKEN;
|
||||
process.env.MATRIX_ACCESS_TOKEN = "token";
|
||||
mocks.readBestEffortConfig.mockResolvedValue({
|
||||
|
|
@ -313,6 +315,8 @@ describe("scanStatus", () => {
|
|||
}
|
||||
}
|
||||
|
||||
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({ scope: "channels" });
|
||||
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
scope: "configured-channels",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ async function scanStatusJsonFast(opts: {
|
|||
});
|
||||
if (hasPotentialConfiguredChannels(cfg)) {
|
||||
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
|
||||
ensurePluginRegistryLoaded({ scope: "channels" });
|
||||
ensurePluginRegistryLoaded({ scope: "configured-channels" });
|
||||
}
|
||||
const osSummary = resolveOsSummary();
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
|
|
|
|||
Loading…
Reference in New Issue