From 51bd95fff3c6b57100e963d7639820f532e6501a Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 08:59:09 +0100 Subject: [PATCH] fix: reuse extractor manifest resolution pass --- CHANGELOG.md | 1 + .../document-extractors.runtime.test.ts | 8 +++ src/plugins/document-extractors.runtime.ts | 50 ++++++++++++------- src/plugins/web-content-extractors.runtime.ts | 50 ++++++++++++------- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd73b41f3f..05b13a2d66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd. - Plugins/channels: resolve read-only channel command defaults from one plugin index plus manifest pass instead of reloading plugin metadata while checking candidate plugin enablement. Thanks @shakkernerd. - Plugins/contracts: resolve runtime manifest-contract plugin owners from one plugin index plus manifest pass instead of rebuilding manifest metadata separately for all owners and enabled owners. Thanks @shakkernerd. +- Plugins/extractors: reuse one manifest registry pass while resolving bundled document and web-content extractor plugins instead of rereading manifests for compatibility and enablement filtering. Thanks @shakkernerd. - Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd. - Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear. - Bonjour/Windows: hide the bundled mDNS advertiser's Windows ARP shell probe so Gateway startup no longer flashes command-prompt windows. Fixes #70238. Thanks @alexandre-leng, @PratikRai0101, @infinitypacific, and @tomerpeled. diff --git a/src/plugins/document-extractors.runtime.test.ts b/src/plugins/document-extractors.runtime.test.ts index 6874e645a47..8fc328d06cc 100644 --- a/src/plugins/document-extractors.runtime.test.ts +++ b/src/plugins/document-extractors.runtime.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { resolvePluginDocumentExtractors } from "./document-extractors.runtime.js"; +import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; vi.mock("./document-extractor-public-artifacts.js", () => ({ loadBundledDocumentExtractorEntriesFromDir: vi.fn( @@ -78,6 +79,13 @@ vi.mock("./manifest-registry.js", () => ({ })); describe("resolvePluginDocumentExtractors", () => { + it("reuses one manifest registry pass for compat and enabled bundled extractors", () => { + vi.mocked(loadPluginManifestRegistryForPluginRegistry).mockClear(); + + expect(resolvePluginDocumentExtractors().map((extractor) => extractor.id)).toEqual(["pdf"]); + expect(loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledOnce(); + }); + it("respects global plugin disablement", () => { expect( resolvePluginDocumentExtractors({ diff --git a/src/plugins/document-extractors.runtime.ts b/src/plugins/document-extractors.runtime.ts index fd56e3c1c8f..58632501357 100644 --- a/src/plugins/document-extractors.runtime.ts +++ b/src/plugins/document-extractors.runtime.ts @@ -22,21 +22,14 @@ function compareExtractors( return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId); } -function resolveBundledDocumentExtractorCompatPluginIds(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; +function listDocumentExtractorPluginIds(params: { + plugins: readonly PluginManifestRecord[]; onlyPluginIds?: readonly string[]; }): string[] { const onlyPluginIdSet = params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadPluginManifestRegistryForPluginRegistry({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }) - .plugins.filter( + return params.plugins + .filter( (plugin) => plugin.origin === "bundled" && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && @@ -46,6 +39,19 @@ function resolveBundledDocumentExtractorCompatPluginIds(params: { .toSorted((left, right) => left.localeCompare(right)); } +function loadDocumentExtractorManifestRecords(params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): readonly PluginManifestRecord[] { + return loadPluginManifestRegistryForPluginRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + includeDisabled: true, + }).plugins; +} + function resolveEnabledBundledDocumentExtractorPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; @@ -55,6 +61,15 @@ function resolveEnabledBundledDocumentExtractorPlugins(params: { if (params.config?.plugins?.enabled === false) { return []; } + let manifestRecords: readonly PluginManifestRecord[] | undefined; + const loadManifestRecords = (config?: OpenClawConfig) => { + manifestRecords ??= loadDocumentExtractorManifestRecords({ + config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + return manifestRecords; + }; const activation = resolveBundledPluginCompatibleLoadValues({ rawConfig: params.config, @@ -67,7 +82,11 @@ function resolveEnabledBundledDocumentExtractorPlugins(params: { enablement: "allowlist", vitest: true, }, - resolveCompatPluginIds: resolveBundledDocumentExtractorCompatPluginIds, + resolveCompatPluginIds: (compatParams) => + listDocumentExtractorPluginIds({ + plugins: loadManifestRecords(compatParams.config), + onlyPluginIds: compatParams.onlyPluginIds, + }), }); const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins); const activationSource = createPluginActivationSource({ @@ -75,12 +94,7 @@ function resolveEnabledBundledDocumentExtractorPlugins(params: { }); const onlyPluginIdSet = params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadPluginManifestRegistryForPluginRegistry({ - config: activation.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }).plugins.filter((plugin) => { + return loadManifestRecords(activation.config).filter((plugin) => { if ( plugin.origin !== "bundled" || (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) || diff --git a/src/plugins/web-content-extractors.runtime.ts b/src/plugins/web-content-extractors.runtime.ts index 08c131ef15e..a1e14908a20 100644 --- a/src/plugins/web-content-extractors.runtime.ts +++ b/src/plugins/web-content-extractors.runtime.ts @@ -22,21 +22,14 @@ function compareExtractors( return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId); } -function resolveBundledWebContentExtractorCompatPluginIds(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; +function listWebContentExtractorPluginIds(params: { + plugins: readonly PluginManifestRecord[]; onlyPluginIds?: readonly string[]; }): string[] { const onlyPluginIdSet = params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadPluginManifestRegistryForPluginRegistry({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }) - .plugins.filter( + return params.plugins + .filter( (plugin) => plugin.origin === "bundled" && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && @@ -46,6 +39,19 @@ function resolveBundledWebContentExtractorCompatPluginIds(params: { .toSorted((left, right) => left.localeCompare(right)); } +function loadWebContentExtractorManifestRecords(params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): readonly PluginManifestRecord[] { + return loadPluginManifestRegistryForPluginRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + includeDisabled: true, + }).plugins; +} + function resolveEnabledBundledExtractorPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; @@ -55,6 +61,15 @@ function resolveEnabledBundledExtractorPlugins(params: { if (params.config?.plugins?.enabled === false) { return []; } + let manifestRecords: readonly PluginManifestRecord[] | undefined; + const loadManifestRecords = (config?: OpenClawConfig) => { + manifestRecords ??= loadWebContentExtractorManifestRecords({ + config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + return manifestRecords; + }; const activation = resolveBundledPluginCompatibleLoadValues({ rawConfig: params.config, @@ -67,7 +82,11 @@ function resolveEnabledBundledExtractorPlugins(params: { enablement: "always", vitest: true, }, - resolveCompatPluginIds: resolveBundledWebContentExtractorCompatPluginIds, + resolveCompatPluginIds: (compatParams) => + listWebContentExtractorPluginIds({ + plugins: loadManifestRecords(compatParams.config), + onlyPluginIds: compatParams.onlyPluginIds, + }), }); const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins); const activationSource = createPluginActivationSource({ @@ -75,12 +94,7 @@ function resolveEnabledBundledExtractorPlugins(params: { }); const onlyPluginIdSet = params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadPluginManifestRegistryForPluginRegistry({ - config: activation.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }).plugins.filter((plugin) => { + return loadManifestRecords(activation.config).filter((plugin) => { if ( plugin.origin !== "bundled" || (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) ||