fix: restore channel sdk schema typing

This commit is contained in:
Peter Steinberger 2026-04-04 08:03:13 +01:00
parent f6df3ed70c
commit 04b539e98c
5 changed files with 97 additions and 49 deletions

View File

@ -43,17 +43,17 @@ let resolveExecHostApprovalContext: typeof import("./bash-tools.exec-host-shared
let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup;
let logWarn: typeof import("../logger.js").logWarn;
describe("sendExecApprovalFollowupResult", () => {
beforeAll(async () => {
({
sendExecApprovalFollowupResult,
MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS: maxExecApprovalFollowupFailureLogKeys,
resolveExecHostApprovalContext,
} = await import("./bash-tools.exec-host-shared.js"));
({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js"));
({ logWarn } = await import("../logger.js"));
});
beforeAll(async () => {
({
sendExecApprovalFollowupResult,
MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS: maxExecApprovalFollowupFailureLogKeys,
resolveExecHostApprovalContext,
} = await import("./bash-tools.exec-host-shared.js"));
({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js"));
({ logWarn } = await import("../logger.js"));
});
describe("sendExecApprovalFollowupResult", () => {
beforeEach(() => {
vi.mocked(sendExecApprovalFollowup).mockReset();
vi.mocked(logWarn).mockReset();

View File

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { buildChannelConfigSchema } from "./config-schema.js";
import { buildChannelConfigSchema, emptyChannelConfigSchema } from "./config-schema.js";
describe("buildChannelConfigSchema", () => {
it("builds json schema when toJSONSchema is available", () => {
@ -46,3 +46,22 @@ describe("buildChannelConfigSchema", () => {
});
});
});
describe("emptyChannelConfigSchema", () => {
it("accepts undefined and empty objects only", () => {
const result = emptyChannelConfigSchema();
expect(result.runtime?.safeParse(undefined)).toEqual({
success: true,
data: undefined,
});
expect(result.runtime?.safeParse({})).toEqual({
success: true,
data: {},
});
expect(result.runtime?.safeParse({ enabled: true })).toEqual({
success: false,
issues: [{ path: [], message: "config must be empty" }],
});
});
});

View File

@ -104,3 +104,33 @@ export function buildChannelConfigSchema(
},
};
}
export function emptyChannelConfigSchema(): ChannelConfigSchema {
return {
schema: {
type: "object",
additionalProperties: false,
properties: {},
},
runtime: {
safeParse(value) {
if (value === undefined) {
return { success: true, data: undefined };
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {
success: false,
issues: [{ path: [], message: "expected config object" }],
};
}
if (Object.keys(value as Record<string, unknown>).length > 0) {
return {
success: false,
issues: [{ path: [], message: "config must be empty" }],
};
}
return { success: true, data: value };
},
},
};
}

View File

@ -1,3 +1,4 @@
import { emptyChannelConfigSchema } from "../channels/plugins/config-schema.js";
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
import {
createScopedAccountReplyToModeResolver,
@ -14,14 +15,17 @@ import type {
ChannelPollResult,
ChannelThreadingAdapter,
} from "../channels/plugins/types.core.js";
import type { ChannelConfigUiHint, ChannelPlugin } from "../channels/plugins/types.plugin.js";
import type {
ChannelConfigSchema,
ChannelConfigUiHint,
ChannelPlugin,
} from "../channels/plugins/types.plugin.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ReplyToMode } from "../config/types.base.js";
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
import type { OpenClawPluginApi } from "../plugins/types.js";
export type { ChannelConfigUiHint, ChannelPlugin };
export type { OpenClawConfig };
@ -31,12 +35,17 @@ export type ChannelOutboundSessionRouteParams = Parameters<
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
>[0];
type ChannelEntryConfigSchema<TPlugin> =
TPlugin extends ChannelPlugin<unknown>
? NonNullable<TPlugin["configSchema"]>
: ChannelConfigSchema;
type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
id: string;
name: string;
description: string;
plugin: TPlugin;
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
configSchema?: ChannelEntryConfigSchema<TPlugin> | (() => ChannelEntryConfigSchema<TPlugin>);
setRuntime?: (runtime: PluginRuntime) => void;
registerCliMetadata?: (api: OpenClawPluginApi) => void;
registerFull?: (api: OpenClawPluginApi) => void;
@ -46,7 +55,7 @@ type DefinedChannelPluginEntry<TPlugin> = {
id: string;
name: string;
description: string;
configSchema: OpenClawPluginConfigSchema;
configSchema: ChannelEntryConfigSchema<TPlugin>;
register: (api: OpenClawPluginApi) => void;
channelPlugin: TPlugin;
setChannelRuntime?: (runtime: PluginRuntime) => void;
@ -270,12 +279,15 @@ export function defineChannelPluginEntry<TPlugin>({
name,
description,
plugin,
configSchema = emptyPluginConfigSchema,
configSchema,
setRuntime,
registerCliMetadata,
registerFull,
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
const resolvedConfigSchema = typeof configSchema === "function" ? configSchema() : configSchema;
const resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> =
typeof configSchema === "function"
? configSchema()
: ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema<TPlugin>);
const entry = {
id,
name,

View File

@ -1,4 +1,5 @@
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../channels/ids.js";
import { emptyChannelConfigSchema } from "../channels/plugins/config-schema.js";
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
import {
createScopedAccountReplyToModeResolver,
@ -22,7 +23,6 @@ import type { ReplyToMode } from "../config/types.base.js";
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
import type { PluginPackageChannel } from "../plugins/manifest.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type { OpenClawPluginApi } from "../plugins/types.js";
@ -144,7 +144,10 @@ export { createDedupeCache, resolveGlobalDedupeCache } from "../infra/dedupe.js"
export { generateSecureToken, generateSecureUuid } from "../infra/secure-random.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
buildChannelConfigSchema,
emptyChannelConfigSchema,
} from "../channels/plugins/config-schema.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
@ -356,35 +359,18 @@ export function buildChannelOutboundSessionRoute(params: {
};
}
const emptyChannelConfigSchema: ChannelConfigSchema = {
schema: {
type: "object",
additionalProperties: false,
properties: {},
},
runtime: {
safeParse(value: unknown) {
if (value === undefined) {
return { success: true, data: undefined };
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { success: false, issues: [{ path: [], message: "expected config object" }] };
}
if (Object.keys(value as Record<string, unknown>).length > 0) {
return { success: false, issues: [{ path: [], message: "config must be empty" }] };
}
return { success: true, data: value };
},
},
};
/** Options for a channel plugin entry that should register a channel capability. */
type ChannelEntryConfigSchema<TPlugin> =
TPlugin extends ChannelPlugin<unknown>
? NonNullable<TPlugin["configSchema"]>
: ChannelConfigSchema;
type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
id: string;
name: string;
description: string;
plugin: TPlugin;
configSchema?: ChannelConfigSchema | (() => ChannelConfigSchema);
configSchema?: ChannelEntryConfigSchema<TPlugin> | (() => ChannelEntryConfigSchema<TPlugin>);
setRuntime?: (runtime: PluginRuntime) => void;
registerCliMetadata?: (api: OpenClawPluginApi) => void;
registerFull?: (api: OpenClawPluginApi) => void;
@ -394,7 +380,7 @@ type DefinedChannelPluginEntry<TPlugin> = {
id: string;
name: string;
description: string;
configSchema: ChannelConfigSchema;
configSchema: ChannelEntryConfigSchema<TPlugin>;
register: (api: OpenClawPluginApi) => void;
channelPlugin: TPlugin;
setChannelRuntime?: (runtime: PluginRuntime) => void;
@ -452,16 +438,17 @@ export function defineChannelPluginEntry<TPlugin>({
name,
description,
plugin,
configSchema = emptyChannelConfigSchema,
configSchema,
setRuntime,
registerCliMetadata,
registerFull,
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
let resolvedConfigSchema: ChannelConfigSchema | undefined;
const getConfigSchema = (): ChannelConfigSchema => {
let resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> | undefined;
const getConfigSchema = (): ChannelEntryConfigSchema<TPlugin> => {
resolvedConfigSchema ??=
(typeof configSchema === "function" ? configSchema() : configSchema) ??
emptyChannelConfigSchema;
typeof configSchema === "function"
? configSchema()
: ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema<TPlugin>);
return resolvedConfigSchema;
};
const entry = {