diff --git a/src/channels/plugins/read-only-command-defaults.ts b/src/channels/plugins/read-only-command-defaults.ts new file mode 100644 index 00000000000..abacae4de21 --- /dev/null +++ b/src/channels/plugins/read-only-command-defaults.ts @@ -0,0 +1,86 @@ +import { isBlockedObjectKey } from "../../infra/prototype-keys.js"; +import type { PluginManifestRecord } from "../../plugins/manifest-registry.js"; +import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import type { ChannelPlugin } from "./types.plugin.js"; + +const SAFE_MANIFEST_CHANNEL_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i; + +export type ChannelCommandDefaults = Pick< + NonNullable, + "nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled" +>; + +type ManifestChannelConfigRecord = NonNullable[string]; + +export function isSafeManifestChannelId(channelId: string): boolean { + return SAFE_MANIFEST_CHANNEL_ID_PATTERN.test(channelId) && !isBlockedObjectKey(channelId); +} + +export function readOwnRecordValue(record: Record, key: string): unknown { + if (isBlockedObjectKey(key) || !Object.prototype.hasOwnProperty.call(record, key)) { + return undefined; + } + return record[key]; +} + +export function normalizeChannelCommandDefaults( + value: ChannelCommandDefaults | undefined, +): ChannelCommandDefaults | undefined { + if (!value) { + return undefined; + } + const nativeCommandsAutoEnabled = + typeof value.nativeCommandsAutoEnabled === "boolean" + ? value.nativeCommandsAutoEnabled + : undefined; + const nativeSkillsAutoEnabled = + typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined; + return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined + ? { + ...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}), + ...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}), + } + : undefined; +} + +export function resolveReadOnlyChannelCommandDefaults( + channelId: string, + options: { + env?: NodeJS.ProcessEnv; + stateDir?: string; + workspaceDir?: string; + } = {}, +): ChannelCommandDefaults | undefined { + const normalizedChannelId = normalizeOptionalString(channelId) ?? ""; + if (!normalizedChannelId || !isSafeManifestChannelId(normalizedChannelId)) { + return undefined; + } + const registry = loadPluginManifestRegistryForPluginRegistry({ + stateDir: options.stateDir, + workspaceDir: options.workspaceDir, + env: options.env ?? process.env, + includeDisabled: true, + }); + for (const record of registry.plugins) { + if (!record.channels.includes(normalizedChannelId)) { + continue; + } + const channelConfigValue = record.channelConfigs + ? readOwnRecordValue(record.channelConfigs as Record, normalizedChannelId) + : undefined; + const channelConfig = + channelConfigValue && + typeof channelConfigValue === "object" && + !Array.isArray(channelConfigValue) + ? (channelConfigValue as ManifestChannelConfigRecord) + : undefined; + const commands = normalizeChannelCommandDefaults( + channelConfig?.commands ?? record.channelCatalogMeta?.commands, + ); + if (commands) { + return commands; + } + } + return undefined; +} diff --git a/src/channels/plugins/read-only.ts b/src/channels/plugins/read-only.ts index 35c8375d636..98149b8bc81 100644 --- a/src/channels/plugins/read-only.ts +++ b/src/channels/plugins/read-only.ts @@ -15,13 +15,17 @@ import type { loadOpenClawPlugins as loadOpenClawPluginsType } from "../../plugi import type { PluginManifestRecord } from "../../plugins/manifest-registry.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { sanitizeForLog } from "../../terminal/ansi.js"; import { getBundledChannelSetupPlugin } from "./bundled.js"; +import { + isSafeManifestChannelId, + normalizeChannelCommandDefaults, + readOwnRecordValue, + resolveReadOnlyChannelCommandDefaults, +} from "./read-only-command-defaults.js"; import { listChannelPlugins } from "./registry.js"; import type { ChannelPlugin } from "./types.plugin.js"; -const SAFE_MANIFEST_CHANNEL_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i; const LOADER_MODULE_CANDIDATES = [ new URL("../../plugins/loader.js", import.meta.url), new URL("../../plugins/loader.ts", import.meta.url), @@ -73,10 +77,6 @@ type ReadOnlyChannelPluginResolution = { missingConfiguredChannelIds: string[]; }; type ManifestChannelConfigRecord = NonNullable[string]; -type ChannelCommandDefaults = Pick< - NonNullable, - "nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled" ->; function addChannelPlugins( byId: Map, @@ -115,41 +115,10 @@ function rebindChannelScopedString( return value; } -function isSafeManifestChannelId(channelId: string): boolean { - return SAFE_MANIFEST_CHANNEL_ID_PATTERN.test(channelId) && !isBlockedObjectKey(channelId); -} - -function readOwnRecordValue(record: Record, key: string): unknown { - if (isBlockedObjectKey(key) || !Object.prototype.hasOwnProperty.call(record, key)) { - return undefined; - } - return record[key]; -} - function normalizeManifestText(value: string | undefined, fallback: string): string { return sanitizeForLog(value?.trim() || fallback).trim(); } -function normalizeChannelCommandDefaults( - value: ChannelCommandDefaults | undefined, -): ChannelCommandDefaults | undefined { - if (!value) { - return undefined; - } - const nativeCommandsAutoEnabled = - typeof value.nativeCommandsAutoEnabled === "boolean" - ? value.nativeCommandsAutoEnabled - : undefined; - const nativeSkillsAutoEnabled = - typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined; - return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined - ? { - ...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}), - ...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}), - } - : undefined; -} - function rebindChannelConfig( cfg: OpenClawConfig, sourceChannelId: string, @@ -347,46 +316,7 @@ function canUseManifestChannelPlugin(record: PluginManifestRecord, channelId: st return record.channelCatalogMeta?.id === channelId; } -export function resolveReadOnlyChannelCommandDefaults( - channelId: string, - options: { - env?: NodeJS.ProcessEnv; - stateDir?: string; - workspaceDir?: string; - } = {}, -): ChannelCommandDefaults | undefined { - const normalizedChannelId = normalizeOptionalString(channelId) ?? ""; - if (!normalizedChannelId || !isSafeManifestChannelId(normalizedChannelId)) { - return undefined; - } - const registry = loadPluginManifestRegistryForPluginRegistry({ - stateDir: options.stateDir, - workspaceDir: options.workspaceDir, - env: options.env ?? process.env, - includeDisabled: true, - }); - for (const record of registry.plugins) { - if (!record.channels.includes(normalizedChannelId)) { - continue; - } - const channelConfigValue = record.channelConfigs - ? readOwnRecordValue(record.channelConfigs as Record, normalizedChannelId) - : undefined; - const channelConfig = - channelConfigValue && - typeof channelConfigValue === "object" && - !Array.isArray(channelConfigValue) - ? (channelConfigValue as ManifestChannelConfigRecord) - : undefined; - const commands = normalizeChannelCommandDefaults( - channelConfig?.commands ?? record.channelCatalogMeta?.commands, - ); - if (commands) { - return commands; - } - } - return undefined; -} +export { resolveReadOnlyChannelCommandDefaults }; function rebindChannelPluginConfig( config: ChannelPlugin["config"], diff --git a/src/config/commands.ts b/src/config/commands.ts index e91ab650be6..14a7b089f4b 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -1,5 +1,5 @@ import { getLoadedChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js"; -import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js"; +import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only-command-defaults.js"; import type { ChannelId } from "../channels/plugins/types.public.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import type { NativeCommandsSetting } from "./types.js"; diff --git a/src/plugins/command-registry-state.ts b/src/plugins/command-registry-state.ts index 5cf85408b5b..0a783974d27 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -1,5 +1,5 @@ import { getLoadedChannelPlugin } from "../channels/plugins/index.js"; -import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js"; +import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only-command-defaults.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import type { OpenClawPluginCommandDefinition } from "./types.js";