diff --git a/extensions/discord/index.ts b/extensions/discord/index.ts index ad441b09bc1..b08a27f80b5 100644 --- a/extensions/discord/index.ts +++ b/extensions/discord/index.ts @@ -12,6 +12,9 @@ const plugin = { register(api: OpenClawPluginApi) { setDiscordRuntime(api.runtime); api.registerChannel({ plugin: discordPlugin }); + if (api.registrationMode !== "full") { + return; + } registerDiscordSubagentHooks(api); }, }; diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 81f8fa9f5e1..8ded5f982ae 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -46,9 +46,11 @@ const { resolveDiscordAllowlistConfigMock, resolveNativeCommandsEnabledMock, resolveNativeSkillsEnabledMock, + shouldLogVerboseMock, voiceRuntimeModuleLoadedMock, } = vi.hoisted(() => { const createdBindingManagers: Array<{ stop: ReturnType }> = []; + const shouldLogVerboseMock = vi.fn(() => false); return { clientHandleDeployRequestMock: vi.fn(async () => undefined), clientConstructorOptionsMock: vi.fn(), @@ -110,6 +112,7 @@ const { })), resolveNativeCommandsEnabledMock: vi.fn(() => true), resolveNativeSkillsEnabledMock: vi.fn(() => false), + shouldLogVerboseMock, voiceRuntimeModuleLoadedMock: vi.fn(), }; }); @@ -211,7 +214,7 @@ vi.mock("../../../../src/config/config.js", () => ({ vi.mock("../../../../src/globals.js", () => ({ danger: (v: string) => v, logVerbose: vi.fn(), - shouldLogVerbose: () => false, + shouldLogVerbose: shouldLogVerboseMock, warn: (v: string) => v, })); @@ -435,6 +438,7 @@ describe("monitorDiscordProvider", () => { }); resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true); resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false); + shouldLogVerboseMock.mockClear().mockReturnValue(false); voiceRuntimeModuleLoadedMock.mockClear(); }); @@ -842,6 +846,7 @@ describe("monitorDiscordProvider", () => { emitter.emit("debug", "WebSocket connection opened"); return { id: "bot-1", username: "Molty" }; }); + shouldLogVerboseMock.mockReturnValue(true); await monitorDiscordProvider({ config: baseConfig(), @@ -861,4 +866,17 @@ describe("monitorDiscordProvider", () => { ), ).toBe(true); }); + + it("keeps Discord startup chatter quiet by default", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + const runtime = baseRuntime(); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime, + }); + + const messages = vi.mocked(runtime.log).mock.calls.map((call) => String(call[0])); + expect(messages.some((msg) => msg.includes("discord startup ["))).toBe(false); + }); }); diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index de174b9d8bf..4f8af71f0d5 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -273,14 +273,18 @@ async function deployDiscordCommands(params: { body === undefined ? undefined : Buffer.byteLength(typeof body === "string" ? body : JSON.stringify(body), "utf8"); - params.runtime.log?.( - `discord startup [${accountId}] deploy-rest:put:start ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path}${typeof commandCount === "number" ? ` commands=${commandCount}` : ""}${typeof bodyBytes === "number" ? ` bytes=${bodyBytes}` : ""}`, - ); + if (shouldLogVerbose()) { + params.runtime.log?.( + `discord startup [${accountId}] deploy-rest:put:start ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path}${typeof commandCount === "number" ? ` commands=${commandCount}` : ""}${typeof bodyBytes === "number" ? ` bytes=${bodyBytes}` : ""}`, + ); + } try { const result = await originalPut(path, data, query); - params.runtime.log?.( - `discord startup [${accountId}] deploy-rest:put:done ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}`, - ); + if (shouldLogVerbose()) { + params.runtime.log?.( + `discord startup [${accountId}] deploy-rest:put:done ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}`, + ); + } return result; } catch (err) { params.runtime.error?.( @@ -359,6 +363,9 @@ function logDiscordStartupPhase(params: { gateway?: GatewayPlugin; details?: string; }) { + if (!shouldLogVerbose()) { + return; + } const elapsedMs = Math.max(0, Date.now() - params.startAt); const suffix = [params.details, formatDiscordStartupGatewayState(params.gateway)] .filter((value): value is string => Boolean(value)) @@ -768,6 +775,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const lifecycleGateway = client.getPlugin("gateway"); earlyGatewayEmitter = getDiscordGatewayEmitter(lifecycleGateway); onEarlyGatewayDebug = (msg: unknown) => { + if (!shouldLogVerbose()) { + return; + } runtime.log?.( `discord startup [${account.accountId}] gateway-debug ${Math.max(0, Date.now() - startupStartedAt)}ms ${String(msg)}`, ); diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index e01a975615a..ba7ac26922b 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -54,6 +54,9 @@ const plugin = { register(api: OpenClawPluginApi) { setFeishuRuntime(api.runtime); api.registerChannel({ plugin: feishuPlugin }); + if (api.registrationMode !== "full") { + return; + } registerFeishuSubagentHooks(api); registerFeishuDocTools(api); registerFeishuChatTools(api); diff --git a/extensions/line/index.ts b/extensions/line/index.ts index 961baf1f01b..59b1d97920d 100644 --- a/extensions/line/index.ts +++ b/extensions/line/index.ts @@ -12,6 +12,9 @@ const plugin = { register(api: OpenClawPluginApi) { setLineRuntime(api.runtime); api.registerChannel({ plugin: linePlugin }); + if (api.registrationMode !== "full") { + return; + } registerLineCardCommand(api); }, }; diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index 7c62501aa6f..bde3767845c 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -32,6 +32,7 @@ function fakeApi(overrides: Partial = {}): OpenClawPluginApi id: "lobster", name: "lobster", source: "test", + registrationMode: "full", config: {}, pluginConfig: {}, // oxlint-disable-next-line typescript/no-explicit-any diff --git a/extensions/mattermost/index.test.ts b/extensions/mattermost/index.test.ts new file mode 100644 index 00000000000..b2ef565c4d2 --- /dev/null +++ b/extensions/mattermost/index.test.ts @@ -0,0 +1,43 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost"; +import { describe, expect, it, vi } from "vitest"; +import { createTestPluginApi } from "../test-utils/plugin-api.js"; +import plugin from "./index.js"; + +function createApi( + registrationMode: OpenClawPluginApi["registrationMode"], + registerHttpRoute = vi.fn(), +): OpenClawPluginApi { + return createTestPluginApi({ + id: "mattermost", + name: "Mattermost", + source: "test", + config: {}, + runtime: {} as OpenClawPluginApi["runtime"], + registrationMode, + registerHttpRoute, + }); +} + +describe("mattermost plugin register", () => { + it("skips slash callback registration in setup-only mode", () => { + const registerHttpRoute = vi.fn(); + + plugin.register(createApi("setup-only", registerHttpRoute)); + + expect(registerHttpRoute).not.toHaveBeenCalled(); + }); + + it("registers slash callback routes in full mode", () => { + const registerHttpRoute = vi.fn(); + + plugin.register(createApi("full", registerHttpRoute)); + + expect(registerHttpRoute).toHaveBeenCalledTimes(1); + expect(registerHttpRoute).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/channels/mattermost/command", + auth: "plugin", + }), + ); + }); +}); diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts index 1dbf616c061..de6f4e1d8a0 100644 --- a/extensions/mattermost/index.ts +++ b/extensions/mattermost/index.ts @@ -12,6 +12,9 @@ const plugin = { register(api: OpenClawPluginApi) { setMattermostRuntime(api.runtime); api.registerChannel({ plugin: mattermostPlugin }); + if (api.registrationMode !== "full") { + return; + } // Register the HTTP route for slash command callbacks. // The actual command registration with MM happens in the monitor diff --git a/extensions/nostr/index.ts b/extensions/nostr/index.ts index aa8901bd2b9..d8fdb203924 100644 --- a/extensions/nostr/index.ts +++ b/extensions/nostr/index.ts @@ -14,6 +14,9 @@ const plugin = { register(api: OpenClawPluginApi) { setNostrRuntime(api.runtime); api.registerChannel({ plugin: nostrPlugin }); + if (api.registrationMode !== "full") { + return; + } // Register HTTP handler for profile management const httpHandler = createNostrProfileHttpHandler({ diff --git a/extensions/test-utils/plugin-api.ts b/extensions/test-utils/plugin-api.ts index a757344bd31..c2eaeced2e5 100644 --- a/extensions/test-utils/plugin-api.ts +++ b/extensions/test-utils/plugin-api.ts @@ -5,6 +5,7 @@ type TestPluginApiInput = Partial & export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi { return { + registrationMode: "full", logger: { info() {}, warn() {}, error() {}, debug() {} }, registerTool() {}, registerHook() {}, diff --git a/extensions/tlon/index.ts b/extensions/tlon/index.ts index 36be4651b1d..2927a9a4b53 100644 --- a/extensions/tlon/index.ts +++ b/extensions/tlon/index.ts @@ -138,6 +138,9 @@ const plugin = { register(api: OpenClawPluginApi) { setTlonRuntime(api.runtime); api.registerChannel({ plugin: tlonPlugin }); + if (api.registrationMode !== "full") { + return; + } api.logger.debug?.("[tlon] Registering tlon tool"); api.registerTool({ diff --git a/extensions/zalouser/index.ts b/extensions/zalouser/index.ts index b169292e954..747a7e26531 100644 --- a/extensions/zalouser/index.ts +++ b/extensions/zalouser/index.ts @@ -12,6 +12,9 @@ const plugin = { register(api: OpenClawPluginApi) { setZalouserRuntime(api.runtime); api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock }); + if (api.registrationMode !== "full") { + return; + } api.registerTool({ name: "zalouser", diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 56abbe79bb4..8e04106dc9c 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -43,6 +43,7 @@ import type { PluginLogger, PluginOrigin, PluginKind, + PluginRegistrationMode, PluginHookName, PluginHookHandlerMap, PluginHookRegistration as TypedPluginHookRegistration, @@ -186,8 +187,6 @@ type PluginTypedHookPolicy = { allowPromptInjection?: boolean; }; -type PluginRegistrationMode = "full" | "setup-only"; - const constrainLegacyPromptInjectionHook = ( handler: PluginHookHandlerMap["before_agent_start"], ): PluginHookHandlerMap["before_agent_start"] => { @@ -734,6 +733,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { description: record.description, source: record.source, rootDir: record.rootDir, + registrationMode, config: params.config, pluginConfig: params.pluginConfig, runtime: registryParams.runtime, diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 6b26dfd8fe6..09a706a51ea 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -839,6 +839,8 @@ export type OpenClawPluginModule = | OpenClawPluginDefinition | ((api: OpenClawPluginApi) => void | Promise); +export type PluginRegistrationMode = "full" | "setup-only"; + export type OpenClawPluginApi = { id: string; name: string; @@ -846,6 +848,7 @@ export type OpenClawPluginApi = { description?: string; source: string; rootDir?: string; + registrationMode: PluginRegistrationMode; config: OpenClawConfig; pluginConfig?: Record; runtime: PluginRuntime;