From fdf7dbd6ebe35946174c56fc5c1b9ba9cd8e0176 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 17:43:24 +0100 Subject: [PATCH] perf(channels): read bundled channel metadata directly --- src/channels/bundled-channel-catalog-read.ts | 114 +++++++++++++++++++ src/channels/chat-meta-shared.ts | 12 +- src/channels/ids.ts | 25 ++-- 3 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 src/channels/bundled-channel-catalog-read.ts diff --git a/src/channels/bundled-channel-catalog-read.ts b/src/channels/bundled-channel-catalog-read.ts new file mode 100644 index 00000000000..4e2d271bee2 --- /dev/null +++ b/src/channels/bundled-channel-catalog-read.ts @@ -0,0 +1,114 @@ +import fs from "node:fs"; +import path from "node:path"; +import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; +import type { PluginPackageChannel } from "../plugins/manifest.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; + +type ChannelCatalogEntryLike = { + openclaw?: { + channel?: PluginPackageChannel; + }; +}; + +export type BundledChannelCatalogEntry = { + id: string; + channel: PluginPackageChannel; + aliases: readonly string[]; + order: number; +}; + +const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = path.join("dist", "channel-catalog.json"); + +function listPackageRoots(): string[] { + return [ + resolveOpenClawPackageRootSync({ cwd: process.cwd() }), + resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url }), + ].filter((entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index); +} + +function listBundledExtensionPackageJsonPaths(): string[] { + for (const packageRoot of listPackageRoots()) { + const extensionsRoot = path.join(packageRoot, "extensions"); + if (!fs.existsSync(extensionsRoot)) { + continue; + } + try { + return fs + .readdirSync(extensionsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(extensionsRoot, entry.name, "package.json")) + .filter((entry) => fs.existsSync(entry)); + } catch { + continue; + } + } + return []; +} + +function readBundledExtensionCatalogEntriesSync(): ChannelCatalogEntryLike[] { + const entries: ChannelCatalogEntryLike[] = []; + for (const packageJsonPath of listBundledExtensionPackageJsonPaths()) { + try { + const payload = JSON.parse( + fs.readFileSync(packageJsonPath, "utf8"), + ) as ChannelCatalogEntryLike; + entries.push(payload); + } catch { + continue; + } + } + return entries; +} + +function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] { + for (const packageRoot of listPackageRoots()) { + const candidate = path.join(packageRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH); + if (!fs.existsSync(candidate)) { + continue; + } + try { + const payload = JSON.parse(fs.readFileSync(candidate, "utf8")) as { + entries?: unknown; + }; + return Array.isArray(payload.entries) ? (payload.entries as ChannelCatalogEntryLike[]) : []; + } catch { + continue; + } + } + return []; +} + +function toBundledChannelEntry(entry: ChannelCatalogEntryLike): BundledChannelCatalogEntry | null { + const channel = entry.openclaw?.channel; + const id = normalizeOptionalLowercaseString(channel?.id); + if (!id || !channel) { + return null; + } + const aliases = Array.isArray(channel.aliases) + ? channel.aliases + .map((alias) => normalizeOptionalLowercaseString(alias)) + .filter((alias): alias is string => Boolean(alias)) + : []; + const order = + typeof channel.order === "number" && Number.isFinite(channel.order) + ? channel.order + : Number.MAX_SAFE_INTEGER; + return { + id, + channel, + aliases, + order, + }; +} + +export function listBundledChannelCatalogEntries(): BundledChannelCatalogEntry[] { + const bundledEntries = readBundledExtensionCatalogEntriesSync() + .map((entry) => toBundledChannelEntry(entry)) + .filter((entry): entry is BundledChannelCatalogEntry => Boolean(entry)); + if (bundledEntries.length > 0) { + return bundledEntries; + } + return readOfficialCatalogFileSync() + .map((entry) => toBundledChannelEntry(entry)) + .filter((entry): entry is BundledChannelCatalogEntry => Boolean(entry)); +} diff --git a/src/channels/chat-meta-shared.ts b/src/channels/chat-meta-shared.ts index a0dc3c641ae..9ddefcd8723 100644 --- a/src/channels/chat-meta-shared.ts +++ b/src/channels/chat-meta-shared.ts @@ -1,6 +1,6 @@ -import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; import type { PluginPackageChannel } from "../plugins/manifest.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { listBundledChannelCatalogEntries } from "./bundled-channel-catalog-read.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js"; import { resolveChannelExposure } from "./plugins/exposure.js"; import type { ChannelMeta } from "./plugins/types.core.js"; @@ -65,12 +65,8 @@ function toChatChannelMeta(params: { export function buildChatChannelMetaById(): Record { const entries = new Map(); - for (const entry of listChannelCatalogEntries({ origin: "bundled" })) { - const channel = entry.channel; - if (!channel) { - continue; - } - const rawId = normalizeOptionalString(channel.id); + for (const entry of listBundledChannelCatalogEntries()) { + const rawId = normalizeOptionalString(entry.id); if (!rawId || !CHAT_CHANNEL_ID_SET.has(rawId)) { continue; } @@ -79,7 +75,7 @@ export function buildChatChannelMetaById(): Record { - const id = normalizeOptionalLowercaseString(channel.id); - if (!id) { - return []; - } - const aliases = (channel.aliases ?? []) - .map((alias) => normalizeOptionalLowercaseString(alias)) - .filter((alias): alias is string => Boolean(alias)); - return [ - { - id, - aliases, - order: typeof channel.order === "number" ? channel.order : Number.MAX_SAFE_INTEGER, - }, - ]; - }) + return listBundledChannelCatalogEntries() + .map((entry) => ({ + id: normalizeOptionalLowercaseString(entry.id) ?? entry.id, + aliases: entry.aliases, + order: entry.order, + })) .toSorted( (left, right) => left.order - right.order || left.id.localeCompare(right.id, "en", { sensitivity: "base" }),