mirror of https://github.com/openclaw/openclaw.git
refactor: collapse bundled channel metadata into plugin manifests
This commit is contained in:
parent
ea60bc01b9
commit
dd098596cf
|
|
@ -646,17 +646,17 @@
|
|||
"canon:check:json": "node scripts/canon.mjs check --json",
|
||||
"canon:enforce": "node scripts/canon.mjs enforce --json",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:bundled-channel-config-metadata && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
"check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs",
|
||||
"config:channels:check": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"config:channels:gen": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --write",
|
||||
"config:channels:check": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"config:channels:gen": "node scripts/generate-bundled-plugin-metadata.mjs",
|
||||
"config:docs:check": "node --import tsx scripts/generate-config-doc-baseline.ts --check",
|
||||
"config:docs:gen": "node --import tsx scripts/generate-config-doc-baseline.ts --write",
|
||||
"config:schema:check": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts";
|
||||
|
||||
const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts";
|
||||
const DEFAULT_OUTPUT_PATH = "src/config/bundled-channel-config-metadata.generated.ts";
|
||||
|
||||
type BundledPluginSource = {
|
||||
dirName: string;
|
||||
pluginDir: string;
|
||||
manifestPath: string;
|
||||
manifest: {
|
||||
id: string;
|
||||
channels?: unknown;
|
||||
name?: string;
|
||||
description?: string;
|
||||
} & Record<string, unknown>;
|
||||
packageJson?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const { collectBundledPluginSources } = (await import(
|
||||
new URL("./lib/bundled-plugin-source-utils.mjs", import.meta.url).href
|
||||
)) as {
|
||||
collectBundledPluginSources: (params?: {
|
||||
repoRoot?: string;
|
||||
requirePackageJson?: boolean;
|
||||
}) => BundledPluginSource[];
|
||||
};
|
||||
|
||||
const { formatGeneratedModule } = (await import(
|
||||
new URL("./lib/format-generated-module.mjs", import.meta.url).href
|
||||
)) as {
|
||||
formatGeneratedModule: (
|
||||
source: string,
|
||||
options: {
|
||||
repoRoot: string;
|
||||
outputPath: string;
|
||||
errorLabel: string;
|
||||
},
|
||||
) => string;
|
||||
};
|
||||
|
||||
const { writeGeneratedOutput } = (await import(
|
||||
new URL("./lib/generated-output-utils.mjs", import.meta.url).href
|
||||
)) as {
|
||||
writeGeneratedOutput: (params: {
|
||||
repoRoot: string;
|
||||
outputPath: string;
|
||||
next: string;
|
||||
check?: boolean;
|
||||
}) => {
|
||||
changed: boolean;
|
||||
wrote: boolean;
|
||||
outputPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BundledChannelConfigMetadata = {
|
||||
pluginId: string;
|
||||
channelId: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function resolveChannelConfigSchemaModulePath(rootDir: string): string | null {
|
||||
const candidates = [
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePackageChannelMeta(source: BundledPluginSource) {
|
||||
const openclawMeta =
|
||||
source.packageJson &&
|
||||
typeof source.packageJson === "object" &&
|
||||
!Array.isArray(source.packageJson) &&
|
||||
"openclaw" in source.packageJson
|
||||
? (source.packageJson.openclaw as Record<string, unknown> | undefined)
|
||||
: undefined;
|
||||
const channelMeta =
|
||||
openclawMeta &&
|
||||
typeof openclawMeta.channel === "object" &&
|
||||
openclawMeta.channel &&
|
||||
!Array.isArray(openclawMeta.channel)
|
||||
? (openclawMeta.channel as Record<string, unknown>)
|
||||
: undefined;
|
||||
return channelMeta;
|
||||
}
|
||||
|
||||
function resolveRootLabel(source: BundledPluginSource, channelId: string): string | undefined {
|
||||
const channelMeta = resolvePackageChannelMeta(source);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.label === "string") {
|
||||
return channelMeta.label.trim();
|
||||
}
|
||||
if (typeof source.manifest?.name === "string" && source.manifest.name.trim()) {
|
||||
return source.manifest.name.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveRootDescription(
|
||||
source: BundledPluginSource,
|
||||
channelId: string,
|
||||
): string | undefined {
|
||||
const channelMeta = resolvePackageChannelMeta(source);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.blurb === "string") {
|
||||
return channelMeta.blurb.trim();
|
||||
}
|
||||
if (typeof source.manifest?.description === "string" && source.manifest.description.trim()) {
|
||||
return source.manifest.description.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatTypeScriptModule(source: string, outputPath: string, repoRoot: string): string {
|
||||
return formatGeneratedModule(source, {
|
||||
repoRoot,
|
||||
outputPath,
|
||||
errorLabel: "bundled channel config metadata",
|
||||
});
|
||||
}
|
||||
|
||||
export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) {
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true });
|
||||
const entries: BundledChannelConfigMetadata[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
const channelIds = Array.isArray(source.manifest?.channels)
|
||||
? source.manifest.channels.filter(
|
||||
(entry: unknown): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
if (channelIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = resolveChannelConfigSchemaModulePath(source.pluginDir);
|
||||
if (!modulePath) {
|
||||
continue;
|
||||
}
|
||||
const surface = await loadChannelConfigSurfaceModule(modulePath, { repoRoot });
|
||||
if (!surface?.schema) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of channelIds) {
|
||||
const label = resolveRootLabel(source, channelId);
|
||||
const description = resolveRootDescription(source, channelId);
|
||||
entries.push({
|
||||
pluginId: String(source.manifest.id),
|
||||
channelId,
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
schema: surface.schema,
|
||||
...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries.toSorted((left, right) => left.channelId.localeCompare(right.channelId));
|
||||
}
|
||||
|
||||
export async function writeBundledChannelConfigMetadataModule(params?: {
|
||||
repoRoot?: string;
|
||||
outputPath?: string;
|
||||
check?: boolean;
|
||||
}) {
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
const outputPath = params?.outputPath ?? DEFAULT_OUTPUT_PATH;
|
||||
const entries = await collectBundledChannelConfigMetadata({ repoRoot });
|
||||
const next = formatTypeScriptModule(
|
||||
`// Auto-generated by ${GENERATED_BY}. Do not edit directly.
|
||||
|
||||
export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = ${JSON.stringify(entries, null, 2)} as const;
|
||||
`,
|
||||
outputPath,
|
||||
repoRoot,
|
||||
);
|
||||
return writeGeneratedOutput({
|
||||
repoRoot,
|
||||
outputPath,
|
||||
next,
|
||||
check: params?.check,
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === new URL(process.argv[1] ?? "", "file://").href) {
|
||||
const check = process.argv.includes("--check");
|
||||
const result = await writeBundledChannelConfigMetadataModule({ check });
|
||||
if (!result.changed) {
|
||||
process.exitCode = 0;
|
||||
} else if (check) {
|
||||
console.error(
|
||||
`[bundled-channel-config-metadata] stale generated output at ${path.relative(process.cwd(), result.outputPath)}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
`[bundled-channel-config-metadata] wrote ${path.relative(process.cwd(), result.outputPath)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,16 +17,17 @@ export type BundledPluginMetadataEntry = {
|
|||
|
||||
export function collectBundledPluginMetadata(params?: {
|
||||
repoRoot?: string;
|
||||
}): BundledPluginMetadataEntry[];
|
||||
}): Promise<BundledPluginMetadataEntry[]>;
|
||||
|
||||
export function renderBundledPluginMetadataModule(entries: BundledPluginMetadataEntry[]): string;
|
||||
|
||||
export function writeBundledPluginMetadataModule(params?: {
|
||||
repoRoot?: string;
|
||||
outputPath?: string;
|
||||
entriesOutputPath?: string;
|
||||
check?: boolean;
|
||||
}): {
|
||||
}): Promise<{
|
||||
changed: boolean;
|
||||
wrote: boolean;
|
||||
outputPath: string;
|
||||
};
|
||||
outputPaths: string[];
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { collectBundledPluginSources } from "./lib/bundled-plugin-source-utils.mjs";
|
||||
import { formatGeneratedModule } from "./lib/format-generated-module.mjs";
|
||||
|
|
@ -134,12 +136,131 @@ function normalizePluginManifest(raw) {
|
|||
...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}),
|
||||
...(typeof raw.version === "string" ? { version: raw.version.trim() } : {}),
|
||||
...(normalizeObject(raw.uiHints) ? { uiHints: raw.uiHints } : {}),
|
||||
...(normalizeObject(raw.channelConfigs) ? { channelConfigs: raw.channelConfigs } : {}),
|
||||
...(normalizeManifestContracts(raw.contracts)
|
||||
? { contracts: normalizeManifestContracts(raw.contracts) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePackageChannelMeta(packageJson) {
|
||||
const openclawMeta =
|
||||
packageJson &&
|
||||
typeof packageJson === "object" &&
|
||||
!Array.isArray(packageJson) &&
|
||||
"openclaw" in packageJson
|
||||
? packageJson.openclaw
|
||||
: undefined;
|
||||
if (!openclawMeta || typeof openclawMeta !== "object" || Array.isArray(openclawMeta)) {
|
||||
return undefined;
|
||||
}
|
||||
const channelMeta = openclawMeta.channel;
|
||||
if (!channelMeta || typeof channelMeta !== "object" || Array.isArray(channelMeta)) {
|
||||
return undefined;
|
||||
}
|
||||
return channelMeta;
|
||||
}
|
||||
|
||||
function resolveChannelConfigSchemaModulePath(rootDir) {
|
||||
const candidates = [
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRootLabel(source, channelId) {
|
||||
const channelMeta = resolvePackageChannelMeta(source.packageJson);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.label === "string") {
|
||||
return channelMeta.label.trim();
|
||||
}
|
||||
if (typeof source.manifest?.name === "string" && source.manifest.name.trim()) {
|
||||
return source.manifest.name.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveRootDescription(source, channelId) {
|
||||
const channelMeta = resolvePackageChannelMeta(source.packageJson);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.blurb === "string") {
|
||||
return channelMeta.blurb.trim();
|
||||
}
|
||||
if (typeof source.manifest?.description === "string" && source.manifest.description.trim()) {
|
||||
return source.manifest.description.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function collectBundledChannelConfigsForSource({ source, manifest }) {
|
||||
const channelIds = Array.isArray(manifest.channels)
|
||||
? manifest.channels.filter((entry) => typeof entry === "string" && entry.trim())
|
||||
: [];
|
||||
const existingChannelConfigs = normalizeObject(manifest.channelConfigs)
|
||||
? { ...manifest.channelConfigs }
|
||||
: {};
|
||||
if (channelIds.length === 0) {
|
||||
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
|
||||
}
|
||||
|
||||
const modulePath = resolveChannelConfigSchemaModulePath(source.pluginDir);
|
||||
if (!modulePath || !fs.existsSync(modulePath)) {
|
||||
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
|
||||
}
|
||||
|
||||
const surfaceJson = execFileSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "scripts/load-channel-config-surface.ts", modulePath],
|
||||
{
|
||||
// Run from the host repo so the generator always resolves its own loader/tooling,
|
||||
// even when inspecting a temporary or alternate repo root.
|
||||
cwd: FORMATTER_CWD,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const surface = JSON.parse(surfaceJson);
|
||||
if (!surface?.schema) {
|
||||
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
|
||||
}
|
||||
|
||||
for (const channelId of channelIds) {
|
||||
const existing =
|
||||
existingChannelConfigs[channelId] &&
|
||||
typeof existingChannelConfigs[channelId] === "object" &&
|
||||
!Array.isArray(existingChannelConfigs[channelId])
|
||||
? existingChannelConfigs[channelId]
|
||||
: undefined;
|
||||
const label = existing?.label ?? resolveRootLabel(source, channelId);
|
||||
const description = existing?.description ?? resolveRootDescription(source, channelId);
|
||||
const uiHints =
|
||||
surface.uiHints || existing?.uiHints
|
||||
? {
|
||||
...(surface.uiHints && Object.keys(surface.uiHints).length > 0
|
||||
? { ...surface.uiHints }
|
||||
: {}),
|
||||
...(existing?.uiHints && Object.keys(existing.uiHints).length > 0
|
||||
? { ...existing.uiHints }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
existingChannelConfigs[channelId] = {
|
||||
schema: surface.schema,
|
||||
...(uiHints && Object.keys(uiHints).length > 0 ? { uiHints } : {}),
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
|
||||
}
|
||||
|
||||
function formatTypeScriptModule(source, { outputPath }) {
|
||||
return formatGeneratedModule(source, {
|
||||
repoRoot: FORMATTER_CWD,
|
||||
|
|
@ -161,7 +282,7 @@ function normalizeGeneratedImportPath(dirName, builtPath) {
|
|||
return `../../extensions/${dirName}/${String(builtPath).replace(/^\.\//u, "")}`;
|
||||
}
|
||||
|
||||
export function collectBundledPluginMetadata(params = {}) {
|
||||
export async function collectBundledPluginMetadata(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const entries = [];
|
||||
for (const source of collectBundledPluginSources({ repoRoot, requirePackageJson: true })) {
|
||||
|
|
@ -192,6 +313,10 @@ export function collectBundledPluginMetadata(params = {}) {
|
|||
built: rewriteEntryToBuiltPath(packageManifest.setupEntry.trim()),
|
||||
}
|
||||
: undefined;
|
||||
const channelConfigs = await collectBundledChannelConfigsForSource({ source, manifest });
|
||||
if (channelConfigs) {
|
||||
manifest.channelConfigs = channelConfigs;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
dirName: source.dirName,
|
||||
|
|
@ -264,9 +389,9 @@ ${imports}
|
|||
`;
|
||||
}
|
||||
|
||||
export function writeBundledPluginMetadataModule(params = {}) {
|
||||
export async function writeBundledPluginMetadataModule(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const entries = collectBundledPluginMetadata({ repoRoot });
|
||||
const entries = await collectBundledPluginMetadata({ repoRoot });
|
||||
const outputPath = path.resolve(repoRoot, params.outputPath ?? DEFAULT_OUTPUT_PATH);
|
||||
const entriesOutputPath = path.resolve(
|
||||
repoRoot,
|
||||
|
|
@ -299,7 +424,7 @@ export function writeBundledPluginMetadataModule(params = {}) {
|
|||
|
||||
if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
|
||||
const check = process.argv.includes("--check");
|
||||
const result = writeBundledPluginMetadataModule({ check });
|
||||
const result = await writeBundledPluginMetadataModule({ check });
|
||||
if (!result.changed) {
|
||||
process.exitCode = 0;
|
||||
} else if (check) {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js";
|
||||
|
||||
type BuiltChannelConfigSurface = {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type JsonSchemaCapableSurface = {
|
||||
toJSONSchema?: (params?: Record<string, unknown>) => unknown;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function isBuiltChannelConfigSchema(value: unknown): value is BuiltChannelConfigSurface {
|
||||
function isBuiltChannelConfigSchema(
|
||||
value: unknown,
|
||||
): value is { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -20,32 +13,9 @@ function isBuiltChannelConfigSchema(value: unknown): value is BuiltChannelConfig
|
|||
return Boolean(candidate.schema && typeof candidate.schema === "object");
|
||||
}
|
||||
|
||||
function buildSchemaSurface(value: unknown): BuiltChannelConfigSurface | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const candidate = value as JsonSchemaCapableSurface;
|
||||
if (typeof candidate.toJSONSchema === "function") {
|
||||
return {
|
||||
schema: candidate.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
}) as Record<string, unknown>,
|
||||
...(candidate.uiHints ? { uiHints: candidate.uiHints } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
},
|
||||
...(candidate.uiHints ? { uiHints: candidate.uiHints } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChannelConfigSurfaceExport(
|
||||
function resolveConfigSchemaExport(
|
||||
imported: Record<string, unknown>,
|
||||
): BuiltChannelConfigSurface | null {
|
||||
): { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } | null {
|
||||
for (const [name, value] of Object.entries(imported)) {
|
||||
if (name.endsWith("ChannelConfigSchema") && isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
|
|
@ -59,9 +29,8 @@ function resolveChannelConfigSurfaceExport(
|
|||
if (isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
const wrapped = buildSchemaSurface(value);
|
||||
if (wrapped) {
|
||||
return wrapped;
|
||||
if (value && typeof value === "object") {
|
||||
return buildChannelConfigSchema(value as never);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +66,8 @@ function shouldRetryViaIsolatedCopy(error: unknown): boolean {
|
|||
return false;
|
||||
}
|
||||
const code = "code" in error ? error.code : undefined;
|
||||
return code === "ERR_MODULE_NOT_FOUND";
|
||||
const message = "message" in error && typeof error.message === "string" ? error.message : "";
|
||||
return code === "ERR_MODULE_NOT_FOUND" && message.includes(`${path.sep}node_modules${path.sep}`);
|
||||
}
|
||||
|
||||
const SOURCE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
||||
|
|
@ -215,7 +185,7 @@ export async function loadChannelConfigSurfaceModule(
|
|||
|
||||
try {
|
||||
const imported = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
|
||||
return resolveChannelConfigSurfaceExport(imported);
|
||||
return resolveConfigSchemaExport(imported);
|
||||
} catch (error) {
|
||||
if (!shouldRetryViaIsolatedCopy(error)) {
|
||||
throw error;
|
||||
|
|
@ -226,7 +196,7 @@ export async function loadChannelConfigSurfaceModule(
|
|||
const imported = (await import(
|
||||
`${pathToFileURL(isolatedCopy.copiedModulePath).href}?isolated=${Date.now()}`
|
||||
)) as Record<string, unknown>;
|
||||
return resolveChannelConfigSurfaceExport(imported);
|
||||
return resolveConfigSchemaExport(imported);
|
||||
} finally {
|
||||
isolatedCopy.cleanup();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { collectBundledChannelConfigMetadata } from "../../scripts/generate-bundled-channel-config-metadata.ts";
|
||||
import { BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.js";
|
||||
|
||||
describe("bundled channel config metadata", () => {
|
||||
it("matches the generated metadata snapshot", async () => {
|
||||
expect(BUNDLED_CHANNEL_CONFIG_METADATA).toEqual(
|
||||
await collectBundledChannelConfigMetadata({ repoRoot: process.cwd() }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.generated.js";
|
||||
import type { ConfigUiHint } from "./schema.hints.js";
|
||||
|
||||
export type BundledChannelConfigMetadata = {
|
||||
pluginId: string;
|
||||
channelId: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, ConfigUiHint>;
|
||||
};
|
||||
|
||||
export const BUNDLED_CHANNEL_CONFIG_METADATA =
|
||||
GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA as unknown as readonly BundledChannelConfigMetadata[];
|
||||
|
|
@ -11790,7 +11790,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
|||
},
|
||||
"agents.list[].runtime.acp.agent": {
|
||||
label: "Agent ACP Harness Agent",
|
||||
help: "Optional ACP harness agent id to use for this OpenClaw agent (for example codex, claude).",
|
||||
help: "Optional ACP harness agent id to use for this OpenClaw agent (for example codex, claude, cursor, gemini, openclaw).",
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"agents.list[].runtime.acp.backend": {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
import type { GroupPolicy } from "./types.base.js";
|
||||
import type { DiscordConfig } from "./types.discord.js";
|
||||
import type { GoogleChatConfig } from "./types.googlechat.js";
|
||||
import type { IMessageConfig } from "./types.imessage.js";
|
||||
import type { IrcConfig } from "./types.irc.js";
|
||||
import type { MSTeamsConfig } from "./types.msteams.js";
|
||||
import type { SignalConfig } from "./types.signal.js";
|
||||
import type { SlackConfig } from "./types.slack.js";
|
||||
import type { TelegramConfig } from "./types.telegram.js";
|
||||
import type { WhatsAppConfig } from "./types.whatsapp.js";
|
||||
|
||||
export type ChannelHeartbeatVisibilityConfig = {
|
||||
/** Show HEARTBEAT_OK acknowledgments in chat (default: false). */
|
||||
|
|
@ -52,20 +43,11 @@ export type ExtensionChannelConfig = {
|
|||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ChannelsConfig = {
|
||||
export interface ChannelsConfig {
|
||||
defaults?: ChannelDefaultsConfig;
|
||||
/** Map provider -> channel id -> model override. */
|
||||
modelByChannel?: ChannelModelByChannelConfig;
|
||||
whatsapp?: WhatsAppConfig;
|
||||
telegram?: TelegramConfig;
|
||||
discord?: DiscordConfig;
|
||||
irc?: IrcConfig;
|
||||
googlechat?: GoogleChatConfig;
|
||||
slack?: SlackConfig;
|
||||
signal?: SignalConfig;
|
||||
imessage?: IMessageConfig;
|
||||
msteams?: MSTeamsConfig;
|
||||
// Extension channels use dynamic keys - use ExtensionChannelConfig in extensions
|
||||
/** Channel sections are plugin-owned; concrete channel files augment this interface. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,3 +377,9 @@ export type DiscordConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & DiscordAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
discord?: DiscordConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,3 +123,9 @@ export type GoogleChatConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & GoogleChatAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
googlechat?: GoogleChatConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,3 +92,9 @@ export type IMessageConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & IMessageAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
imessage?: IMessageConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,3 +59,9 @@ export type IrcConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & IrcAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
irc?: IrcConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,3 +134,9 @@ export type MSTeamsConfig = {
|
|||
/** Minimum interval (ms) between reflections per session. Default: 300000 (5 min). */
|
||||
feedbackReflectionCooldownMs?: number;
|
||||
};
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
msteams?: MSTeamsConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,3 +60,9 @@ export type SignalConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & SignalAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
signal?: SignalConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,3 +207,9 @@ export type SlackConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & SlackAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
slack?: SlackConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,3 +298,9 @@ export type TelegramConfig = {
|
|||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & TelegramAccountConfig;
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
telegram?: TelegramConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,3 +119,9 @@ export type WhatsAppAccountConfig = WhatsAppConfigCore &
|
|||
/** Override auth directory (Baileys multi-file auth state). */
|
||||
authDir?: string;
|
||||
};
|
||||
|
||||
declare module "./types.channels.js" {
|
||||
interface ChannelsConfig {
|
||||
whatsapp?: WhatsAppConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,8 +19,10 @@ import {
|
|||
installGeneratedPluginTempRootCleanup();
|
||||
|
||||
describe("bundled plugin metadata", () => {
|
||||
it("matches the generated metadata snapshot", () => {
|
||||
expect(BUNDLED_PLUGIN_METADATA).toEqual(collectBundledPluginMetadata({ repoRoot }));
|
||||
it("matches the generated metadata snapshot", async () => {
|
||||
await expect(collectBundledPluginMetadata({ repoRoot })).resolves.toEqual(
|
||||
BUNDLED_PLUGIN_METADATA,
|
||||
);
|
||||
});
|
||||
|
||||
it("captures setup-entry metadata for bundled channel plugins", () => {
|
||||
|
|
@ -28,6 +30,11 @@ describe("bundled plugin metadata", () => {
|
|||
expect(discord?.source).toEqual({ source: "./index.ts", built: "index.js" });
|
||||
expect(discord?.setupSource).toEqual({ source: "./setup-entry.ts", built: "setup-entry.js" });
|
||||
expect(discord?.manifest.id).toBe("discord");
|
||||
expect(discord?.manifest.channelConfigs?.discord).toEqual(
|
||||
expect.objectContaining({
|
||||
schema: expect.objectContaining({ type: "object" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers built generated paths when present and falls back to source paths", () => {
|
||||
|
|
@ -51,7 +58,7 @@ describe("bundled plugin metadata", () => {
|
|||
).toBe(path.join(tempRoot, "plugin", "index.js"));
|
||||
});
|
||||
|
||||
it("supports check mode for stale generated artifacts", () => {
|
||||
it("supports check mode for stale generated artifacts", async () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-generated-");
|
||||
|
||||
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
|
||||
|
|
@ -66,13 +73,13 @@ describe("bundled plugin metadata", () => {
|
|||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const initial = writeBundledPluginMetadataModule({
|
||||
const initial = await writeBundledPluginMetadataModule({
|
||||
repoRoot: tempRoot,
|
||||
outputPath: "src/plugins/bundled-plugin-metadata.generated.ts",
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
const current = writeBundledPluginMetadataModule({
|
||||
const current = await writeBundledPluginMetadataModule({
|
||||
repoRoot: tempRoot,
|
||||
outputPath: "src/plugins/bundled-plugin-metadata.generated.ts",
|
||||
check: true,
|
||||
|
|
@ -86,7 +93,7 @@ describe("bundled plugin metadata", () => {
|
|||
"utf8",
|
||||
);
|
||||
|
||||
const stale = writeBundledPluginMetadataModule({
|
||||
const stale = await writeBundledPluginMetadataModule({
|
||||
repoRoot: tempRoot,
|
||||
outputPath: "src/plugins/bundled-plugin-metadata.generated.ts",
|
||||
check: true,
|
||||
|
|
@ -94,4 +101,78 @@ describe("bundled plugin metadata", () => {
|
|||
expect(stale.changed).toBe(true);
|
||||
expect(stale.wrote).toBe(false);
|
||||
});
|
||||
|
||||
it("merges generated channel schema metadata with manifest-owned channel config fields", async () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-channel-configs-");
|
||||
|
||||
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
|
||||
name: "@openclaw/alpha",
|
||||
version: "0.0.1",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
channel: {
|
||||
id: "alpha",
|
||||
label: "Alpha Root Label",
|
||||
blurb: "Alpha Root Description",
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
|
||||
id: "alpha",
|
||||
channels: ["alpha"],
|
||||
configSchema: { type: "object" },
|
||||
channelConfigs: {
|
||||
alpha: {
|
||||
schema: { type: "object", properties: { stale: { type: "boolean" } } },
|
||||
label: "Manifest Label",
|
||||
uiHints: {
|
||||
"channels.alpha.explicitOnly": {
|
||||
help: "manifest hint",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "extensions", "alpha", "index.ts"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.mkdirSync(path.join(tempRoot, "extensions", "alpha", "src"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "extensions", "alpha", "src", "config-schema.js"),
|
||||
[
|
||||
"export const AlphaChannelConfigSchema = {",
|
||||
" schema: {",
|
||||
" type: 'object',",
|
||||
" properties: { generated: { type: 'string' } },",
|
||||
" },",
|
||||
" uiHints: {",
|
||||
" 'channels.alpha.generatedOnly': { help: 'generated hint' },",
|
||||
" },",
|
||||
"};",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const entries = await collectBundledPluginMetadata({ repoRoot: tempRoot });
|
||||
const channelConfigs = entries[0]?.manifest.channelConfigs as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
expect(channelConfigs?.alpha).toEqual({
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
generated: { type: "string" },
|
||||
},
|
||||
},
|
||||
label: "Manifest Label",
|
||||
description: "Alpha Root Description",
|
||||
uiHints: {
|
||||
"channels.alpha.generatedOnly": { help: "generated hint" },
|
||||
"channels.alpha.explicitOnly": { help: "manifest hint" },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ function createPluginCandidate(params: {
|
|||
bundleFormat?: "codex" | "claude" | "cursor";
|
||||
packageManifest?: OpenClawPackageManifest;
|
||||
packageDir?: string;
|
||||
bundledManifest?: PluginCandidate["bundledManifest"];
|
||||
bundledManifestPath?: string;
|
||||
}): PluginCandidate {
|
||||
return {
|
||||
idHint: params.idHint,
|
||||
|
|
@ -52,6 +54,8 @@ function createPluginCandidate(params: {
|
|||
bundleFormat: params.bundleFormat,
|
||||
packageManifest: params.packageManifest,
|
||||
packageDir: params.packageDir,
|
||||
bundledManifest: params.bundledManifest,
|
||||
bundledManifestPath: params.bundledManifestPath,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -338,17 +342,24 @@ describe("loadPluginManifestRegistry", () => {
|
|||
|
||||
it("hydrates bundled channel config metadata onto manifest records", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "telegram",
|
||||
channels: ["telegram"],
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "telegram",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
});
|
||||
const registry = loadRegistry([
|
||||
createPluginCandidate({
|
||||
idHint: "telegram",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
bundledManifestPath: path.join(dir, "openclaw.plugin.json"),
|
||||
bundledManifest: {
|
||||
id: "telegram",
|
||||
configSchema: { type: "object" },
|
||||
channels: ["telegram"],
|
||||
channelConfigs: {
|
||||
telegram: {
|
||||
schema: { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(registry.plugins[0]?.channelConfigs?.telegram).toEqual(
|
||||
expect.objectContaining({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { BUNDLED_CHANNEL_CONFIG_METADATA } from "../config/bundled-channel-config-metadata.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
|
|
@ -169,7 +168,6 @@ function buildRecord(params: {
|
|||
schemaCacheKey?: string;
|
||||
configSchema?: Record<string, unknown>;
|
||||
}): PluginManifestRecord {
|
||||
const bundledChannelConfigs = resolveBundledChannelConfigs(params.manifest.id);
|
||||
return {
|
||||
id: params.manifest.id,
|
||||
name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.packageName,
|
||||
|
|
@ -201,7 +199,7 @@ function buildRecord(params: {
|
|||
configSchema: params.configSchema,
|
||||
configUiHints: params.manifest.uiHints,
|
||||
contracts: params.manifest.contracts,
|
||||
channelConfigs: mergeChannelConfigs(bundledChannelConfigs, params.manifest.channelConfigs),
|
||||
channelConfigs: params.manifest.channelConfigs,
|
||||
...(params.candidate.packageManifest?.channel?.id
|
||||
? {
|
||||
channelCatalogMeta: {
|
||||
|
|
@ -221,40 +219,6 @@ function buildRecord(params: {
|
|||
};
|
||||
}
|
||||
|
||||
function resolveBundledChannelConfigs(
|
||||
pluginId: string,
|
||||
): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
const entries = BUNDLED_CHANNEL_CONFIG_METADATA.filter((entry) => entry.pluginId === pluginId);
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
entries.map((entry) => [
|
||||
entry.channelId,
|
||||
{
|
||||
schema: entry.schema,
|
||||
...(entry.uiHints ? { uiHints: entry.uiHints } : {}),
|
||||
...(entry.label ? { label: entry.label } : {}),
|
||||
...(entry.description ? { description: entry.description } : {}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function mergeChannelConfigs(
|
||||
generated: Record<string, PluginManifestChannelConfig> | undefined,
|
||||
manifest: Record<string, PluginManifestChannelConfig> | undefined,
|
||||
): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
if (!generated) {
|
||||
return manifest;
|
||||
}
|
||||
if (!manifest) {
|
||||
return generated;
|
||||
}
|
||||
return { ...generated, ...manifest };
|
||||
}
|
||||
|
||||
function buildBundleRecord(params: {
|
||||
manifest: {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue