Main recovery: restore formatter and contract checks (#49570)

* Extensions: fix oxfmt drift on main

* Plugins: restore runtime barrel exports on main

* Config: restore web search compatibility types

* Telegram: align test harness with reply runtime

* Plugin SDK: fix channel config accessor generics

* CLI: remove redundant search provider casts

* Tests: restore main typecheck coverage

* Lobster: fix test import formatting

* Extensions: route bundled seams through plugin-sdk

* Tests: use extension env helper for xai

* Image generation: fix main oxfmt drift

* Config: restore latest main compatibility checks

* Plugin SDK: align guardrail tests with lint

* Telegram: type native command skill mock
This commit is contained in:
Vincent Koc 2026-03-18 00:30:01 -07:00 committed by GitHub
parent e6c6aaa11b
commit fbd88e2c8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 476 additions and 327 deletions

View File

@ -1,4 +1,4 @@
import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "../runtime-api.js";
import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "../../runtime-api.js";
import {
asOptionalBoolean,
asOptionalString,

View File

@ -25,7 +25,7 @@ describe("amazon-bedrock provider plugin", () => {
const wrapped = provider.wrapStreamFn?.({
provider: "amazon-bedrock",
modelId: "amazon.nova-micro-v1:0",
streamFn: (_model, _context, options) => options,
streamFn: (_model: unknown, _context: unknown, options: Record<string, unknown>) => options,
} as never);
expect(

View File

@ -132,8 +132,8 @@ function resolveBraveConfig(
: ({ apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey } as BraveConfig);
}
function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" {
return brave.mode === "llm-context" ? "llm-context" : "web";
function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
return brave?.mode === "llm-context" ? "llm-context" : "web";
}
function resolveBraveApiKey(

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/copilot-proxy.js";
export * from "openclaw/plugin-sdk/copilot-proxy";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/device-pair.js";
export * from "openclaw/plugin-sdk/device-pair";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/diagnostics-otel.js";
export * from "openclaw/plugin-sdk/diagnostics-otel";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/diffs.js";
export * from "openclaw/plugin-sdk/diffs";

View File

@ -1,18 +1,16 @@
import {
applyDirectoryQueryAndLimit,
collectNormalizedDirectoryIds,
inspectReadOnlyChannelAccount,
toDirectoryEntries,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import type { InspectedDiscordAccount } from "../api.js";
import { inspectDiscordAccount, type InspectedDiscordAccount } from "../api.js";
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "discord",
const account = inspectDiscordAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedDiscordAccount | null;
}) as InspectedDiscordAccount | null;
if (!account || !("config" in account)) {
return [];
}
@ -34,11 +32,10 @@ export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfi
}
export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "discord",
const account = inspectDiscordAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedDiscordAccount | null;
}) as InspectedDiscordAccount | null;
if (!account || !("config" in account)) {
return [];
}

View File

@ -40,6 +40,7 @@ export type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "openclaw/plugin-sdk/channel-runtime";
export type { DiscordConfig } from "openclaw/plugin-sdk/discord";
export {
assertMediaNotDataUrl,
parseAvailableTags,

View File

@ -1 +1 @@
export { normalizeGoogleModelId, parseGeminiAuth } from "openclaw/plugin-sdk/google";
export * from "openclaw/plugin-sdk/google";

View File

@ -1,23 +1,19 @@
export type { IMessageAccountConfig } from "../../src/config/types.imessage.js";
export type { ChannelPlugin } from "../../src/channels/plugins/types.plugin.js";
export {
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
buildChannelConfigSchema,
getChatChannelMeta,
} from "../../src/plugin-sdk/channel-plugin-common.js";
export {
collectStatusIssuesFromLastError,
formatTrimmedAllowFromEntries,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
} from "../../src/plugin-sdk/channel-config-helpers.js";
export { collectStatusIssuesFromLastError } from "../../src/plugin-sdk/status-helpers.js";
export { resolveChannelMediaMaxBytes } from "../../src/channels/plugins/media-limits.js";
export {
getChatChannelMeta,
looksLikeIMessageTargetId,
normalizeIMessageMessagingTarget,
} from "../../src/channels/plugins/normalize/imessage.js";
export { IMessageConfigSchema } from "../../src/config/zod-schema.providers-core.js";
resolveChannelMediaMaxBytes,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
IMessageConfigSchema,
type ChannelPlugin,
type IMessageAccountConfig,
} from "openclaw/plugin-sdk/imessage";
export {
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,

View File

@ -1,53 +1 @@
export {
addWildcardAllowFrom,
buildBaseAccountStatusSnapshot,
buildBaseChannelStatusSummary,
buildChannelConfigSchema,
createAccountListHelpers,
createAccountStatusSink,
createLoggerBackedRuntime,
createNormalizedOutboundDeliverer,
createReplyPrefixOptions,
createScopedPairingAccess,
dispatchInboundReplyWithBase,
emptyPluginConfigSchema,
formatDocsLink,
formatPairingApproveHint,
formatTextWithAttachmentLinks,
getChatChannelMeta,
GROUP_POLICY_BLOCKED_LABEL,
isDangerousNameMatchingEnabled,
issuePairingChallenge,
logInboundDrop,
normalizeResolvedSecretInputString,
parseOptionalDelimitedEntries,
PAIRING_APPROVED_MESSAGE,
patchScopedAccountConfig,
readStoreAllowFromForDmPolicy,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveControlCommandGate,
resolveDefaultGroupPolicy,
resolveEffectiveAllowFromLists,
resolveOutboundMediaUrls,
runPassiveAccountLifecycle,
setAccountEnabledInConfigSection,
setTopLevelChannelAllowFrom,
setTopLevelChannelDmPolicyWithAllowFrom,
ToolPolicySchema,
warnMissingProviderGroupPolicyFallbackOnce,
type BaseProbeResult,
type BlockStreamingCoalesceConfig,
type ChannelPlugin,
type DmConfig,
type DmPolicy,
type GroupPolicy,
type GroupToolPolicyBySenderConfig,
type GroupToolPolicyConfig,
type MarkdownConfig,
type OpenClawConfig,
type OpenClawPluginApi,
type OutboundReplyPayload,
type PluginRuntime,
type RuntimeEnv,
type WizardPrompter,
} from "openclaw/plugin-sdk/irc";
export * from "openclaw/plugin-sdk/irc";

View File

@ -12,6 +12,7 @@ import {
type ChannelStatusIssue,
type LineConfig,
type LineChannelData,
type OpenClawConfig,
type ResolvedLineAccount,
} from "../api.js";
import { lineConfigAdapter } from "./config-adapter.js";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/llm-task.js";
export * from "openclaw/plugin-sdk/llm-task";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/lobster.js";
export * from "openclaw/plugin-sdk/lobster";

View File

@ -3,8 +3,8 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { PassThrough } from "node:stream";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../runtime-api.js";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../runtime-api.js";
import {
createWindowsCmdShimFixture,
restorePlatformPathEnv,

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/memory-lancedb.js";
export * from "openclaw/plugin-sdk/memory-lancedb";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/open-prose.js";
export * from "openclaw/plugin-sdk/open-prose";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/phone-control.js";
export * from "openclaw/plugin-sdk/phone-control";

View File

@ -1,5 +1,7 @@
import { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin-sdk/agent-runtime";
import { QWEN_OAUTH_MARKER } from "openclaw/plugin-sdk/agent-runtime";
import { loginQwenPortalOAuth } from "./oauth.js";
import { buildQwenPortalProvider, QWEN_PORTAL_BASE_URL } from "./provider-catalog.js";
import {
buildOauthProviderAuthResult,
definePluginEntry,
@ -7,8 +9,6 @@ import {
type ProviderAuthContext,
type ProviderCatalogContext,
} from "./runtime-api.js";
import { loginQwenPortalOAuth } from "./oauth.js";
import { buildQwenPortalProvider, QWEN_PORTAL_BASE_URL } from "./provider-catalog.js";
const PROVIDER_ID = "qwen-portal";
const PROVIDER_LABEL = "Qwen";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/qwen-portal-auth.js";
export * from "openclaw/plugin-sdk/qwen-portal-auth";

View File

@ -1,6 +1,6 @@
import { type ResolvedSignalAccount } from "./accounts.js";
import { signalSetupAdapter } from "./setup-core.js";
import { type ChannelPlugin } from "./runtime-api.js";
import { signalSetupAdapter } from "./setup-core.js";
import { createSignalPluginBase, signalSetupWizard } from "./shared.js";
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = {

View File

@ -4,6 +4,16 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import { buildOutboundBaseSessionKey } from "openclaw/plugin-sdk/core";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
import { type RoutePeer } from "openclaw/plugin-sdk/routing";
import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js";
import { markdownToSignalTextChunks } from "./format.js";
import {
looksLikeUuid,
resolveSignalPeerId,
resolveSignalRecipient,
resolveSignalSender,
} from "./identity.js";
import { signalMessageActions } from "./message-actions.js";
import type { SignalProbe } from "./probe.js";
import {
buildBaseAccountStatusSnapshot,
buildBaseChannelStatusSummary,
@ -17,16 +27,6 @@ import {
resolveChannelMediaMaxBytes,
type ChannelPlugin,
} from "./runtime-api.js";
import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js";
import { markdownToSignalTextChunks } from "./format.js";
import {
looksLikeUuid,
resolveSignalPeerId,
resolveSignalRecipient,
resolveSignalSender,
} from "./identity.js";
import { signalMessageActions } from "./message-actions.js";
import type { SignalProbe } from "./probe.js";
import { getSignalRuntime } from "./runtime.js";
import { signalSetupAdapter } from "./setup-core.js";
import {

View File

@ -4,6 +4,12 @@ import {
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import {
buildChannelConfigSchema,
getChatChannelMeta,
@ -11,12 +17,6 @@ import {
SignalConfigSchema,
type ChannelPlugin,
} from "./runtime-api.js";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import { createSignalSetupWizardProxy } from "./setup-core.js";
export const SIGNAL_CHANNEL = "signal" as const;

View File

@ -29,6 +29,8 @@ import { resolveSlackUserAllowlist } from "./resolve-users.js";
import {
buildComputedAccountStatusSnapshot,
DEFAULT_ACCOUNT_ID,
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
PAIRING_APPROVED_MESSAGE,

View File

@ -1,20 +1,18 @@
import {
applyDirectoryQueryAndLimit,
collectNormalizedDirectoryIds,
inspectReadOnlyChannelAccount,
listDirectoryGroupEntriesFromMapKeys,
toDirectoryEntries,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import type { InspectedSlackAccount } from "../api.js";
import { inspectSlackAccount, type InspectedSlackAccount } from "../api.js";
import { parseSlackTarget } from "./targets.js";
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "slack",
const account = inspectSlackAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedSlackAccount | null;
}) as InspectedSlackAccount | null;
if (!account || !("config" in account)) {
return [];
}
@ -40,11 +38,10 @@ export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigP
}
export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "slack",
const account = inspectSlackAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedSlackAccount | null;
}) as InspectedSlackAccount | null;
if (!account || !("config" in account)) {
return [];
}

View File

@ -1,34 +1,29 @@
export type { OpenClawConfig } from "../../../src/config/config.js";
export type { SlackAccountConfig } from "../../../src/config/types.slack.js";
export type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
export {
buildComputedAccountStatusSnapshot,
DEFAULT_ACCOUNT_ID,
buildChannelConfigSchema,
getChatChannelMeta,
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
PAIRING_APPROVED_MESSAGE,
} from "../../../src/plugin-sdk/channel-plugin-common.js";
export { buildComputedAccountStatusSnapshot } from "../../../src/plugin-sdk/status-helpers.js";
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
type ChannelPlugin,
type OpenClawConfig,
type SlackAccountConfig,
} from "openclaw/plugin-sdk/slack";
export {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "./directory-config.js";
export {
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
} from "../../../src/channels/plugins/normalize/slack.js";
export {
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
} from "../../../src/channels/account-snapshot-fields.js";
export { SlackConfigSchema } from "../../../src/config/zod-schema.providers-core.js";
export {
buildChannelConfigSchema,
getChatChannelMeta,
createActionGate,
imageResultFromFile,
jsonResult,
readNumberParam,
readReactionParams,
readStringParam,
} from "../../../src/agents/tools/common.js";
export { withNormalizedTimestamp } from "../../../src/agents/date-time.js";
SlackConfigSchema,
withNormalizedTimestamp,
} from "openclaw/plugin-sdk/slack-core";
export { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/talk-voice.js";
export * from "openclaw/plugin-sdk/talk-voice";

View File

@ -1,16 +1,18 @@
export type {
ChannelMessageActionAdapter,
ChannelPlugin,
OpenClawConfig,
TelegramActionConfig,
} from "../../src/plugin-sdk/telegram-core.js";
export type { ChannelMessageActionAdapter } from "../../src/channels/plugins/types.js";
export type { TelegramAccountConfig, TelegramNetworkConfig } from "../../src/config/types.js";
export type {
OpenClawPluginApi,
PluginRuntime,
TelegramAccountConfig,
TelegramActionConfig,
TelegramNetworkConfig,
} from "openclaw/plugin-sdk/telegram";
export type {
OpenClawPluginService,
OpenClawPluginServiceContext,
PluginLogger,
} from "../../src/plugins/types.js";
} from "openclaw/plugin-sdk/core";
export type {
AcpRuntime,
AcpRuntimeCapabilities,
@ -20,12 +22,22 @@ export type {
AcpRuntimeHandle,
AcpRuntimeStatus,
AcpRuntimeTurnInput,
AcpRuntimeErrorCode,
AcpSessionUpdateTag,
} from "../../src/acp/runtime/types.js";
export type { AcpRuntimeErrorCode } from "../../src/acp/runtime/errors.js";
export { AcpRuntimeError } from "../../src/acp/runtime/errors.js";
} from "openclaw/plugin-sdk/acp-runtime";
export { AcpRuntimeError } from "openclaw/plugin-sdk/acp-runtime";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../src/routing/session-key.js";
export {
buildTokenChannelStatusSummary,
clearAccountEntryFields,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
parseTelegramTopicConversation,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
resolveTelegramPollVisibility,
} from "openclaw/plugin-sdk/telegram";
export {
buildChannelConfigSchema,
getChatChannelMeta,
@ -37,13 +49,31 @@ export {
readStringParam,
resolvePollMaxSelections,
TelegramConfigSchema,
} from "../../src/plugin-sdk/telegram-core.js";
export { parseTelegramTopicConversation } from "../../src/acp/conversation-id.js";
export { clearAccountEntryFields } from "../../src/channels/plugins/config-helpers.js";
export { buildTokenChannelStatusSummary } from "../../src/plugin-sdk/status-helpers.js";
} from "openclaw/plugin-sdk/telegram-core";
export type { TelegramProbe } from "./src/probe.js";
export { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds } from "./src/audit.js";
export { telegramMessageActions } from "./src/channel-actions.js";
export { monitorTelegramProvider } from "./src/monitor.js";
export { probeTelegram } from "./src/probe.js";
export {
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
} from "../../src/channels/account-snapshot-fields.js";
export { resolveTelegramPollVisibility } from "../../src/poll-params.js";
export { PAIRING_APPROVED_MESSAGE } from "../../src/channels/plugins/pairing-message.js";
createForumTopicTelegram,
deleteMessageTelegram,
editForumTopicTelegram,
editMessageReplyMarkupTelegram,
editMessageTelegram,
pinMessageTelegram,
reactMessageTelegram,
renameForumTopicTelegram,
sendMessageTelegram,
sendPollTelegram,
sendStickerTelegram,
sendTypingTelegram,
unpinMessageTelegram,
} from "./src/send.js";
export {
createTelegramThreadBindingManager,
getTelegramThreadBindingManager,
setTelegramThreadBindingIdleTimeoutBySessionKey,
setTelegramThreadBindingMaxAgeBySessionKey,
} from "./src/thread-bindings.js";
export { resolveTelegramToken } from "./src/token.js";

View File

@ -1,5 +1,6 @@
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { expect, vi } from "vitest";
import type { SkillCommandSpec } from "../../../src/agents/skills.js";
import type { OpenClawConfig } from "../runtime-api.js";
import type { TelegramBotDeps } from "./bot-deps.js";
import {
@ -8,6 +9,12 @@ import {
type NativeCommandTestParams as RegisterTelegramNativeCommandsParams,
} from "./bot-native-commands.fixture-test-support.js";
const EMPTY_REPLY_COUNTS = {
block: 0,
final: 0,
tool: 0,
} as const;
type RegisteredCommand = {
command: string;
description: string;
@ -21,7 +28,9 @@ type CreateCommandBotResult = {
};
const skillCommandMocks = vi.hoisted(() => ({
listSkillCommandsForAgents: vi.fn(() => []),
listSkillCommandsForAgents: vi.fn<
(params: { cfg: OpenClawConfig; agentIds?: string[] }) => SkillCommandSpec[]
>(() => []),
}));
const deliveryMocks = vi.hoisted(() => ({
@ -86,7 +95,7 @@ export function createNativeCommandTestParams(
enqueueSystemEvent: vi.fn(),
dispatchReplyWithBufferedBlockDispatcher: vi.fn(async () => ({
queuedFinal: false,
counts: {},
counts: EMPTY_REPLY_COUNTS,
})),
listSkillCommandsForAgents,
wasSentByBot: vi.fn(() => false),

View File

@ -37,6 +37,12 @@ import {
waitForRegisteredCommands,
} from "./bot-native-commands.menu-test-support.js";
const EMPTY_REPLY_COUNTS = {
block: 0,
final: 0,
tool: 0,
} as const;
function createNativeCommandTestParams(
cfg: OpenClawConfig,
params: Partial<Parameters<typeof registerTelegramNativeCommands>[0]> = {},
@ -48,7 +54,7 @@ function createNativeCommandTestParams(
enqueueSystemEvent: vi.fn(),
dispatchReplyWithBufferedBlockDispatcher: vi.fn(async () => ({
queuedFinal: false,
counts: {},
counts: EMPTY_REPLY_COUNTS,
})),
listSkillCommandsForAgents: skillCommandMocks.listSkillCommandsForAgents,
wasSentByBot: vi.fn(() => false),

View File

@ -4,23 +4,21 @@ import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import type { MockFn } from "openclaw/plugin-sdk/testing";
import { beforeEach, vi } from "vitest";
import type { TelegramBotDeps } from "./bot-deps.js";
type AnyMock = MockFn<(...args: unknown[]) => unknown>;
type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise<unknown>>;
type AnyMock = ReturnType<typeof vi.fn>;
type AnyAsyncMock = ReturnType<typeof vi.fn>;
type DispatchReplyWithBufferedBlockDispatcherFn =
typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher;
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;
type DispatchReplyHarnessParams = {
ctx: MsgContext;
replyOptions?: GetReplyOptions;
dispatcherOptions?: {
typingCallbacks?: {
start?: () => void | Promise<void>;
};
deliver?: (payload: ReplyPayload, info: { kind: "final" }) => void | Promise<void>;
};
type DispatchReplyHarnessParams = Parameters<DispatchReplyWithBufferedBlockDispatcherFn>[0];
const EMPTY_REPLY_COUNTS: DispatchReplyWithBufferedBlockDispatcherResult["counts"] = {
block: 0,
final: 0,
tool: 0,
};
const { sessionStorePath } = vi.hoisted(() => ({
@ -39,12 +37,14 @@ vi.doMock("openclaw/plugin-sdk/web-media", () => ({
loadWebMedia,
}));
const { loadConfig } = vi.hoisted((): { loadConfig: AnyMock } => ({
loadConfig: vi.fn(() => ({})),
}));
const { resolveStorePathMock } = vi.hoisted((): { resolveStorePathMock: AnyMock } => ({
resolveStorePathMock: vi.fn((storePath?: string) => storePath ?? sessionStorePath),
const { loadConfig } = vi.hoisted((): { loadConfig: MockFn<() => OpenClawConfig> } => ({
loadConfig: vi.fn(() => ({}) as OpenClawConfig),
}));
const { resolveStorePathMock } = vi.hoisted(
(): { resolveStorePathMock: MockFn<TelegramBotDeps["resolveStorePath"]> } => ({
resolveStorePathMock: vi.fn((storePath?: string) => storePath ?? sessionStorePath),
}),
);
export function getLoadConfigMock(): AnyMock {
return loadConfig;
@ -67,7 +67,7 @@ vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(
(): {
readChannelAllowFromStore: AnyAsyncMock;
readChannelAllowFromStore: MockFn<TelegramBotDeps["readChannelAllowFromStore"]>;
upsertChannelPairingRequest: AnyAsyncMock;
} => ({
readChannelAllowFromStore: vi.fn(async () => [] as string[]),
@ -111,9 +111,9 @@ const skillCommandsHoisted = vi.hoisted(() => ({
async (params: DispatchReplyHarnessParams) => {
const result: DispatchReplyWithBufferedBlockDispatcherResult = {
queuedFinal: false,
counts: {} as DispatchReplyWithBufferedBlockDispatcherResult["counts"],
counts: EMPTY_REPLY_COUNTS,
};
await params.dispatcherOptions?.typingCallbacks?.start?.();
await params.dispatcherOptions?.typingCallbacks?.onReplyStart?.();
const reply = await skillCommandsHoisted.replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {
@ -141,9 +141,10 @@ vi.doMock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
});
const systemEventsHoisted = vi.hoisted(() => ({
enqueueSystemEventSpy: vi.fn(),
enqueueSystemEventSpy: vi.fn<TelegramBotDeps["enqueueSystemEvent"]>(() => false),
}));
export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy;
export const enqueueSystemEventSpy: MockFn<TelegramBotDeps["enqueueSystemEvent"]> =
systemEventsHoisted.enqueueSystemEventSpy;
vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
@ -173,7 +174,7 @@ const grammySpies = vi.hoisted(() => ({
onSpy: vi.fn() as AnyMock,
stopSpy: vi.fn() as AnyMock,
commandSpy: vi.fn() as AnyMock,
botCtorSpy: vi.fn() as AnyMock,
botCtorSpy: vi.fn((_: string, __?: { client?: { fetch?: typeof fetch } }) => undefined),
answerCallbackQuerySpy: vi.fn(async () => undefined) as AnyAsyncMock,
sendChatActionSpy: vi.fn() as AnyMock,
editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock,
@ -191,26 +192,26 @@ const grammySpies = vi.hoisted(() => ({
getFileSpy: vi.fn(async () => ({ file_path: "media/file.jpg" })) as AnyAsyncMock,
}));
export const {
useSpy,
middlewareUseSpy,
onSpy,
stopSpy,
commandSpy,
botCtorSpy,
answerCallbackQuerySpy,
sendChatActionSpy,
editMessageTextSpy,
editMessageReplyMarkupSpy,
sendMessageDraftSpy,
setMessageReactionSpy,
setMyCommandsSpy,
getMeSpy,
sendMessageSpy,
sendAnimationSpy,
sendPhotoSpy,
getFileSpy,
} = grammySpies;
export const useSpy: MockFn<(arg: unknown) => void> = grammySpies.useSpy;
export const middlewareUseSpy: AnyMock = grammySpies.middlewareUseSpy;
export const onSpy: AnyMock = grammySpies.onSpy;
export const stopSpy: AnyMock = grammySpies.stopSpy;
export const commandSpy: AnyMock = grammySpies.commandSpy;
export const botCtorSpy: MockFn<
(token: string, options?: { client?: { fetch?: typeof fetch } }) => void
> = grammySpies.botCtorSpy;
export const answerCallbackQuerySpy: AnyAsyncMock = grammySpies.answerCallbackQuerySpy;
export const sendChatActionSpy: AnyMock = grammySpies.sendChatActionSpy;
export const editMessageTextSpy: AnyAsyncMock = grammySpies.editMessageTextSpy;
export const editMessageReplyMarkupSpy: AnyAsyncMock = grammySpies.editMessageReplyMarkupSpy;
export const sendMessageDraftSpy: AnyAsyncMock = grammySpies.sendMessageDraftSpy;
export const setMessageReactionSpy: AnyAsyncMock = grammySpies.setMessageReactionSpy;
export const setMyCommandsSpy: AnyAsyncMock = grammySpies.setMyCommandsSpy;
export const getMeSpy: AnyAsyncMock = grammySpies.getMeSpy;
export const sendMessageSpy: AnyAsyncMock = grammySpies.sendMessageSpy;
export const sendAnimationSpy: AnyAsyncMock = grammySpies.sendAnimationSpy;
export const sendPhotoSpy: AnyAsyncMock = grammySpies.sendPhotoSpy;
export const getFileSpy: AnyAsyncMock = grammySpies.getFileSpy;
const runnerHoisted = vi.hoisted(() => ({
sequentializeMiddleware: vi.fn(async (_ctx: unknown, next?: () => Promise<void>) => {
@ -224,7 +225,11 @@ const runnerHoisted = vi.hoisted(() => ({
export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy;
export let sequentializeKey: ((ctx: unknown) => string) | undefined;
export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy;
export const telegramBotRuntimeForTest = {
export const telegramBotRuntimeForTest: {
Bot: new (token: string, options?: { client?: { fetch?: typeof fetch } }) => unknown;
sequentialize: (keyFn: (ctx: unknown) => string) => unknown;
apiThrottler: () => unknown;
} = {
Bot: class {
api = {
config: { use: grammySpies.useSpy },
@ -259,7 +264,7 @@ export const telegramBotRuntimeForTest = {
},
apiThrottler: () => runnerHoisted.throttlerSpy(),
};
export const telegramBotDepsForTest = {
export const telegramBotDepsForTest: TelegramBotDeps = {
loadConfig,
resolveStorePath: resolveStorePathMock,
readChannelAllowFromStore,
@ -365,9 +370,9 @@ beforeEach(() => {
async (params: DispatchReplyHarnessParams) => {
const result: DispatchReplyWithBufferedBlockDispatcherResult = {
queuedFinal: false,
counts: {} as DispatchReplyWithBufferedBlockDispatcherResult["counts"],
counts: EMPTY_REPLY_COUNTS,
};
await params.dispatcherOptions?.typingCallbacks?.start?.();
await params.dispatcherOptions?.typingCallbacks?.onReplyStart?.();
const reply = await replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {

View File

@ -39,7 +39,9 @@ const {
getTelegramSequentialKey,
setTelegramBotRuntimeForTest,
} = await import("./bot.js");
setTelegramBotRuntimeForTest(telegramBotRuntimeForTest);
setTelegramBotRuntimeForTest(
telegramBotRuntimeForTest as unknown as Parameters<typeof setTelegramBotRuntimeForTest>[0],
);
const createTelegramBot = (opts: Parameters<typeof createTelegramBotBase>[0]) =>
createTelegramBotBase({
...opts,

View File

@ -6,7 +6,9 @@ const { botCtorSpy, telegramBotDepsForTest } =
const { telegramBotRuntimeForTest } = await import("./bot.create-telegram-bot.test-harness.js");
const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
await import("./bot.js");
setTelegramBotRuntimeForTest(telegramBotRuntimeForTest);
setTelegramBotRuntimeForTest(
telegramBotRuntimeForTest as unknown as Parameters<typeof setTelegramBotRuntimeForTest>[0],
);
const createTelegramBot = (opts: Parameters<typeof createTelegramBotBase>[0]) =>
createTelegramBotBase({
...opts,

View File

@ -1,5 +1,12 @@
import { resetInboundDedupe } from "openclaw/plugin-sdk/reply-runtime";
import { beforeEach, vi, type Mock } from "vitest";
import type { TelegramBotDeps } from "./bot-deps.js";
const EMPTY_REPLY_COUNTS = {
block: 0,
final: 0,
tool: 0,
} as const;
export const useSpy: Mock = vi.fn();
export const middlewareUseSpy: Mock = vi.fn();
@ -56,7 +63,11 @@ const apiStub: ApiStub = {
setMyCommands: vi.fn(async () => undefined),
};
export const telegramBotRuntimeForTest = {
export const telegramBotRuntimeForTest: {
Bot: new (token: string) => unknown;
sequentialize: () => unknown;
apiThrottler: () => unknown;
} = {
Bot: class {
api = apiStub;
use = middlewareUseSpy;
@ -84,12 +95,12 @@ const mediaHarnessDispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() =>
for (const payload of payloads) {
await params.dispatcherOptions?.deliver?.(payload, { kind: "final" });
}
return { queuedFinal: false, counts: {} };
return { queuedFinal: false, counts: EMPTY_REPLY_COUNTS };
}),
);
export const telegramBotDepsForTest = {
export const telegramBotDepsForTest: TelegramBotDeps = {
loadConfig: () => ({
channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] } },
channels: { telegram: { dmPolicy: "open" as const, allowFrom: ["*"] } },
}),
resolveStorePath: vi.fn((storePath?: string) => storePath ?? "/tmp/telegram-media-sessions.json"),
readChannelAllowFromStore: vi.fn(async () => [] as string[]),

View File

@ -107,7 +107,11 @@ beforeAll(async () => {
onSpyRef = harness.onSpy;
sendChatActionSpyRef = harness.sendChatActionSpy;
const botModule = await import("./bot.js");
botModule.setTelegramBotRuntimeForTest(harness.telegramBotRuntimeForTest);
botModule.setTelegramBotRuntimeForTest(
harness.telegramBotRuntimeForTest as unknown as Parameters<
typeof botModule.setTelegramBotRuntimeForTest
>[0],
);
createTelegramBotRef = (opts) =>
botModule.createTelegramBot({
...opts,

View File

@ -35,7 +35,9 @@ const { normalizeTelegramCommandName } =
await import("../../../src/config/telegram-custom-commands.js");
const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
await import("./bot.js");
setTelegramBotRuntimeForTest(telegramBotRuntimeForTest);
setTelegramBotRuntimeForTest(
telegramBotRuntimeForTest as unknown as Parameters<typeof setTelegramBotRuntimeForTest>[0],
);
const createTelegramBot = (opts: Parameters<typeof createTelegramBotBase>[0]) =>
createTelegramBotBase({
...opts,

View File

@ -2,19 +2,17 @@ import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"
import {
applyDirectoryQueryAndLimit,
collectNormalizedDirectoryIds,
inspectReadOnlyChannelAccount,
listDirectoryGroupEntriesFromMapKeys,
toDirectoryEntries,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import type { InspectedTelegramAccount } from "../api.js";
import { inspectTelegramAccount, type InspectedTelegramAccount } from "../api.js";
export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "telegram",
const account = inspectTelegramAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedTelegramAccount | null;
}) as InspectedTelegramAccount | null;
if (!account || !("config" in account)) {
return [];
}
@ -36,11 +34,10 @@ export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConf
}
export async function listTelegramDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
const account = (await inspectReadOnlyChannelAccount({
channelId: "telegram",
const account = inspectTelegramAccount({
cfg: params.cfg,
accountId: params.accountId,
})) as InspectedTelegramAccount | null;
}) as InspectedTelegramAccount | null;
if (!account || !("config" in account)) {
return [];
}

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/thread-ownership.js";
export * from "openclaw/plugin-sdk/thread-ownership";

View File

@ -1,2 +1 @@
export * from "openclaw/plugin-sdk/twitch";
export * from "./src/setup-surface.js";

View File

@ -1 +1 @@
export * from "../../src/plugin-sdk/voice-call.js";
export * from "openclaw/plugin-sdk/voice-call";

View File

@ -1,22 +1,30 @@
export {
buildChannelConfigSchema,
createActionGate,
createWhatsAppOutboundBase,
DEFAULT_ACCOUNT_ID,
formatWhatsAppConfigAllowFromEntries,
isWhatsAppGroupJid,
getChatChannelMeta,
jsonResult,
normalizeWhatsAppTarget,
normalizeE164,
readReactionParams,
readStringParam,
resolveWhatsAppHeartbeatRecipients,
resolveWhatsAppMentionStripRegexes,
resolveWhatsAppGroupIntroHint,
resolveWhatsAppOutboundTarget,
ToolAuthorizationError,
WhatsAppConfigSchema,
type ChannelPlugin,
type OpenClawConfig,
} from "openclaw/plugin-sdk/whatsapp-core";
export {
createWhatsAppOutboundBase,
isWhatsAppGroupJid,
normalizeWhatsAppTarget,
resolveWhatsAppHeartbeatRecipients,
resolveWhatsAppMentionStripRegexes,
type ChannelMessageActionName,
type DmPolicy,
type GroupPolicy,
type OpenClawConfig,
type WhatsAppAccountConfig,
} from "openclaw/plugin-sdk/whatsapp";

View File

@ -3,7 +3,7 @@ import {
resolveWebSearchProviderCredential,
} from "openclaw/plugin-sdk/provider-web-search";
import { describe, expect, it } from "vitest";
import { withEnv } from "../../src/test-utils/env.js";
import { withEnv } from "../../test/helpers/extensions/env.js";
import { __testing } from "./web-search.js";
const { extractXaiWebSearchContent, resolveXaiInlineCitations, resolveXaiWebSearchModel } =

View File

@ -1,11 +1,11 @@
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import { listEnabledZaloAccounts } from "./accounts.js";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
OpenClawConfig,
} from "./runtime-api.js";
import { extractToolSend, jsonResult, readStringParam } from "./runtime-api.js";
import { listEnabledZaloAccounts } from "./accounts.js";
const loadZaloActionsRuntime = createLazyRuntimeNamedExport(
() => import("./actions.runtime.js"),

View File

@ -1,18 +1,15 @@
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
import { probeZalo } from "./probe.js";
import { resolveZaloProxyFetch } from "./proxy.js";
import { normalizeSecretInputString } from "./secret-input.js";
import { sendMessageZalo } from "./send.js";
import {
PAIRING_APPROVED_MESSAGE,
type ChannelPlugin,
type OpenClawConfig,
} from "./runtime-api.js";
import { normalizeSecretInputString } from "./secret-input.js";
import { sendMessageZalo } from "./send.js";
export async function notifyZaloPairingApproval(params: {
cfg: OpenClawConfig;
id: string;
}) {
export async function notifyZaloPairingApproval(params: { cfg: OpenClawConfig; id: string }) {
const { resolveZaloAccount } = await import("./accounts.js");
const account = resolveZaloAccount({ cfg: params.cfg });
if (!account.token) {
@ -44,11 +41,7 @@ export async function probeZaloAccount(params: {
}
export async function startZaloGatewayAccount(
ctx: Parameters<
NonNullable<
NonNullable<ChannelPlugin["gateway"]>["startAccount"]
>
>[0],
ctx: Parameters<NonNullable<NonNullable<ChannelPlugin["gateway"]>["startAccount"]>>[0],
) {
const account = ctx.account;
const token = account.token.trim();

View File

@ -9,6 +9,14 @@ import {
collectOpenProviderGroupPolicyWarnings,
} from "openclaw/plugin-sdk/channel-policy";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import {
listZaloAccountIds,
resolveDefaultZaloAccountId,
resolveZaloAccount,
type ResolvedZaloAccount,
} from "./accounts.js";
import { zaloMessageActions } from "./actions.js";
import { ZaloConfigSchema } from "./config-schema.js";
import {
buildBaseAccountStatusSnapshot,
buildChannelConfigSchema,
@ -24,14 +32,6 @@ import {
type ChannelPlugin,
type OpenClawConfig,
} from "./runtime-api.js";
import {
listZaloAccountIds,
resolveDefaultZaloAccountId,
resolveZaloAccount,
type ResolvedZaloAccount,
} from "./accounts.js";
import { zaloMessageActions } from "./actions.js";
import { ZaloConfigSchema } from "./config-schema.js";
import { resolveZaloOutboundSessionRoute } from "./session-route.js";
import { zaloSetupAdapter } from "./setup-core.js";
import { zaloSetupWizard } from "./setup-surface.js";

View File

@ -5,8 +5,8 @@ import {
GroupPolicySchema,
} from "openclaw/plugin-sdk/channel-config-schema";
import { z } from "zod";
import { buildSecretInputSchema } from "./secret-input.js";
import { MarkdownConfigSchema } from "./runtime-api.js";
import { buildSecretInputSchema } from "./secret-input.js";
const zaloAccountSchema = z.object({
name: z.string().optional(),

View File

@ -1,25 +1,4 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type {
MarkdownTableMode,
OpenClawConfig,
OutboundReplyPayload,
} from "./runtime-api.js";
import {
createTypingCallbacks,
createScopedPairingAccess,
createReplyPrefixOptions,
issuePairingChallenge,
logTypingFailure,
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
resolveOutboundMediaUrls,
resolveDefaultGroupPolicy,
resolveInboundRouteEnvelopeBuilderWithRuntime,
sendMediaWithLeadingCaption,
resolveWebhookPath,
waitForAbortSignal,
warnMissingProviderGroupPolicyFallbackOnce,
} from "./runtime-api.js";
import type { ResolvedZaloAccount } from "./accounts.js";
import {
ZaloApiError,
@ -48,6 +27,23 @@ import {
type ZaloWebhookTarget,
} from "./monitor.webhook.js";
import { resolveZaloProxyFetch } from "./proxy.js";
import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "./runtime-api.js";
import {
createTypingCallbacks,
createScopedPairingAccess,
createReplyPrefixOptions,
issuePairingChallenge,
logTypingFailure,
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
resolveOutboundMediaUrls,
resolveDefaultGroupPolicy,
resolveInboundRouteEnvelopeBuilderWithRuntime,
sendMediaWithLeadingCaption,
resolveWebhookPath,
waitForAbortSignal,
warnMissingProviderGroupPolicyFallbackOnce,
} from "./runtime-api.js";
import { getZaloRuntime } from "./runtime.js";
export type ZaloRuntimeEnv = {

View File

@ -1,5 +1,8 @@
import { timingSafeEqual } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ResolvedZaloAccount } from "./accounts.js";
import type { ZaloFetch, ZaloUpdate } from "./api.js";
import type { ZaloRuntimeEnv } from "./monitor.js";
import {
createDedupeCache,
createFixedWindowRateLimiter,
@ -17,9 +20,6 @@ import {
resolveClientIp,
type OpenClawConfig,
} from "./runtime-api.js";
import type { ResolvedZaloAccount } from "./accounts.js";
import type { ZaloFetch, ZaloUpdate } from "./api.js";
import type { ZaloRuntimeEnv } from "./monitor.js";
const ZALO_WEBHOOK_REPLAY_WINDOW_MS = 5 * 60_000;

View File

@ -2,8 +2,8 @@ import { resolveZaloAccount } from "./accounts.js";
import type { ZaloFetch } from "./api.js";
import { sendMessage, sendPhoto } from "./api.js";
import { resolveZaloProxyFetch } from "./proxy.js";
import { resolveZaloToken } from "./token.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { resolveZaloToken } from "./token.js";
export type ZaloSendOptions = {
token?: string;

View File

@ -1,8 +1,8 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/infra-runtime";
import type { BaseTokenResolution } from "./runtime-api.js";
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
import type { ZaloConfig } from "./types.js";
import type { BaseTokenResolution } from "./runtime-api.js";
export type ZaloTokenResolution = BaseTokenResolution & {
source: "env" | "config" | "configFile" | "none";

View File

@ -17,7 +17,17 @@ function stubImageGenerationProviders() {
id: "openai",
defaultModel: "gpt-image-1",
models: ["gpt-image-1"],
supportedSizes: ["1024x1024"],
capabilities: {
generate: {
supportsSize: true,
},
edit: {
enabled: false,
},
geometry: {
sizes: ["1024x1024"],
},
},
generateImage: vi.fn(async () => {
throw new Error("not used");
}),

View File

@ -18,7 +18,7 @@ describe("extra-params: Google thinking payload compatibility", () => {
api: "google-generative-ai",
provider: "google",
id: "gemini-3.1-pro-preview",
} as Model<"openai-completions">,
} as unknown as Model<"openai-completions">,
thinkingLevel: "high",
payload: {
contents: [],

View File

@ -457,7 +457,7 @@ describe("createOpenClawCodingTools", () => {
it("applies xai model compat for direct Grok tool cleanup", () => {
const xaiTools = createOpenClawCodingTools({
modelProvider: "xai",
modelCompat: applyXaiModelCompat({}).compat,
modelCompat: applyXaiModelCompat({ compat: {} }).compat,
senderIsOwner: true,
});

View File

@ -18,10 +18,7 @@ function toolNames(tools: AnyAgentTool[]): string[] {
describe("applyModelProviderToolPolicy", () => {
it("keeps web_search for non-xAI models", () => {
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
modelProvider: "openai",
modelId: "gpt-4o-mini",
});
const filtered = __testing.applyModelProviderToolPolicy(baseTools);
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
});

View File

@ -392,10 +392,11 @@ describe("createImageGenerateTool", () => {
throw new Error("expected image_generate tool");
}
await expect(tool.execute("call-bad-aspect", { prompt: "portrait", aspectRatio: "7:5" }))
.rejects.toThrow(
"aspectRatio must be one of 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, or 21:9",
);
await expect(
tool.execute("call-bad-aspect", { prompt: "portrait", aspectRatio: "7:5" }),
).rejects.toThrow(
"aspectRatio must be one of 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, or 21:9",
);
});
it("lists registered provider and model options", async () => {

View File

@ -230,7 +230,9 @@ function normalizeReferenceImages(args: Record<string, unknown>): string[] {
return normalized;
}
function parseImageGenerationModelRef(raw: string | undefined): { provider: string; model: string } | null {
function parseImageGenerationModelRef(
raw: string | undefined,
): { provider: string; model: string } | null {
const trimmed = raw?.trim();
if (!trimmed) {
return null;
@ -258,7 +260,8 @@ function resolveSelectedImageGenerationProvider(params: {
}
return listRuntimeImageGenerationProviders({ config: params.config }).find(
(provider) =>
provider.id === selectedRef.provider || (provider.aliases ?? []).includes(selectedRef.provider),
provider.id === selectedRef.provider ||
(provider.aliases ?? []).includes(selectedRef.provider),
);
}
@ -298,7 +301,9 @@ function validateImageGenerationCapabilities(params: {
if (params.size) {
if (!modeCaps.supportsSize) {
throw new ToolInputError(`${provider.id} ${isEdit ? "edit" : "generate"} does not support size overrides.`);
throw new ToolInputError(
`${provider.id} ${isEdit ? "edit" : "generate"} does not support size overrides.`,
);
}
if ((geometry?.sizes?.length ?? 0) > 0 && !geometry?.sizes?.includes(params.size)) {
throw new ToolInputError(
@ -309,7 +314,9 @@ function validateImageGenerationCapabilities(params: {
if (params.aspectRatio) {
if (!modeCaps.supportsAspectRatio) {
throw new ToolInputError(`${provider.id} ${isEdit ? "edit" : "generate"} does not support aspectRatio overrides.`);
throw new ToolInputError(
`${provider.id} ${isEdit ? "edit" : "generate"} does not support aspectRatio overrides.`,
);
}
if (
(geometry?.aspectRatios?.length ?? 0) > 0 &&
@ -323,7 +330,9 @@ function validateImageGenerationCapabilities(params: {
if (params.resolution) {
if (!modeCaps.supportsResolution) {
throw new ToolInputError(`${provider.id} ${isEdit ? "edit" : "generate"} does not support resolution overrides.`);
throw new ToolInputError(
`${provider.id} ${isEdit ? "edit" : "generate"} does not support resolution overrides.`,
);
}
if (
(geometry?.resolutions?.length ?? 0) > 0 &&

View File

@ -26,7 +26,7 @@ type AssistantLikeMessage = {
};
function resolveLiveXaiModel() {
return getModel("xai", "grok-4-1-fast-reasoning") ?? getModel("xai", "grok-4");
return getModel("xai", "grok-4");
}
async function collectDoneMessage(

View File

@ -722,7 +722,14 @@ export function createAccountScopedGroupAccessSection<TResolved>(params: {
};
}
type AccountScopedChannel = "discord" | "slack" | "telegram" | "imessage" | "signal";
type AccountScopedChannel =
| "bluebubbles"
| "discord"
| "imessage"
| "line"
| "signal"
| "slack"
| "telegram";
type LegacyDmChannel = "discord" | "slack";
export function patchLegacyDmChannelConfig(params: {

View File

@ -1,7 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginCompatibilityNotice } from "../plugins/status.js";
const readConfigFileSnapshot = vi.fn();
const buildPluginCompatibilityNotices = vi.fn(() => []);
const buildPluginCompatibilityNotices = vi.fn((): PluginCompatibilityNotice[] => []);
vi.mock("../config/config.js", () => ({
readConfigFileSnapshot,

View File

@ -184,13 +184,13 @@ async function promptWebToolsConfig(
if (!entry) {
return false;
}
return hasExistingKey(nextConfig, provider as SP) || hasKeyInEnv(entry);
return hasExistingKey(nextConfig, provider) || hasKeyInEnv(entry);
};
const existingProvider: SP = (() => {
const stored = existingSearch?.provider;
if (stored && SEARCH_PROVIDER_OPTIONS.some((e) => e.value === stored)) {
return stored as SP;
return stored;
}
return (
SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? defaultProvider
@ -242,8 +242,8 @@ async function promptWebToolsConfig(
nextSearch = { ...nextSearch, provider: providerChoice };
const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === providerChoice)!;
const existingKey = resolveExistingKey(nextConfig, providerChoice as SP);
const keyConfigured = hasExistingKey(nextConfig, providerChoice as SP);
const existingKey = resolveExistingKey(nextConfig, providerChoice);
const keyConfigured = hasExistingKey(nextConfig, providerChoice);
const envAvailable = entry.envKeys.some((k) => Boolean(process.env[k]?.trim()));
const envVarNames = entry.envKeys.join(" / ");
@ -263,7 +263,7 @@ async function promptWebToolsConfig(
const key = String(keyInput ?? "").trim();
if (key || existingKey) {
const applied = applySearchKey(nextConfig, providerChoice as SP, (key || existingKey)!);
const applied = applySearchKey(nextConfig, providerChoice, (key || existingKey)!);
nextSearch = { ...applied.tools?.web?.search };
} else if (keyConfigured || envAvailable) {
nextSearch = { ...nextSearch };

View File

@ -359,6 +359,8 @@ describe("normalizeCompatibilityConfigValues", () => {
providers: {
google: {
apiKey: "existing-google-key",
baseUrl: "https://generativelanguage.googleapis.com",
models: [],
},
},
},

View File

@ -474,6 +474,11 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
};
const normalizeLegacyNanoBananaSkill = () => {
type ModelProviderEntry = Partial<
NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]>[string]
>;
type ModelsConfigPatch = Partial<NonNullable<OpenClawConfig["models"]>>;
const rawSkills = next.skills;
if (!isRecord(rawSkills)) {
return;
@ -544,14 +549,20 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
? structuredClone(rawLegacyEntry.apiKey)
: undefined);
const rawModels = isRecord(next.models) ? structuredClone(next.models) : {};
const rawProviders = isRecord(rawModels.providers) ? { ...rawModels.providers } : {};
const rawGoogle = isRecord(rawProviders.google) ? { ...rawProviders.google } : {};
const rawModels = (
isRecord(next.models) ? structuredClone(next.models) : {}
) as ModelsConfigPatch;
const rawProviders = (
isRecord(rawModels.providers) ? { ...rawModels.providers } : {}
) as Record<string, ModelProviderEntry>;
const rawGoogle = (
isRecord(rawProviders.google) ? { ...rawProviders.google } : {}
) as ModelProviderEntry;
const hasGoogleApiKey = rawGoogle.apiKey !== undefined;
if (!hasGoogleApiKey && legacyApiKey) {
rawGoogle.apiKey = legacyApiKey;
rawProviders.google = rawGoogle;
rawModels.providers = rawProviders;
rawModels.providers = rawProviders as NonNullable<OpenClawConfig["models"]>["providers"];
next = {
...next,
models: rawModels as OpenClawConfig["models"],

View File

@ -444,6 +444,14 @@ export type MemorySearchConfig = {
};
};
type WebSearchLegacyProviderConfig = {
apiKey?: SecretInput;
baseUrl?: string;
model?: string;
mode?: string;
inlineCitations?: boolean;
};
export type ToolsConfig = {
/** Base tool profile applied before allow/deny lists. */
profile?: ToolProfileId;
@ -465,6 +473,20 @@ export type ToolsConfig = {
timeoutSeconds?: number;
/** Cache TTL in minutes for search results. */
cacheTtlMinutes?: number;
/** @deprecated Legacy Brave credential path. */
apiKey?: SecretInput;
/** @deprecated Legacy Brave scoped config. */
brave?: WebSearchLegacyProviderConfig;
/** @deprecated Legacy Firecrawl scoped config. */
firecrawl?: WebSearchLegacyProviderConfig;
/** @deprecated Legacy Gemini scoped config. */
gemini?: WebSearchLegacyProviderConfig;
/** @deprecated Legacy Grok scoped config. */
grok?: WebSearchLegacyProviderConfig;
/** @deprecated Legacy Kimi scoped config. */
kimi?: WebSearchLegacyProviderConfig;
/** @deprecated Legacy Perplexity scoped config. */
perplexity?: WebSearchLegacyProviderConfig;
};
fetch?: {
/** Enable web fetch tool (default: true). */

View File

@ -267,6 +267,57 @@ export const ToolsWebSearchSchema = z
maxResults: z.number().int().positive().optional(),
timeoutSeconds: z.number().int().positive().optional(),
cacheTtlMinutes: z.number().nonnegative().optional(),
apiKey: SecretInputSchema.optional().register(sensitive),
brave: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
mode: z.string().optional(),
})
.strict()
.optional(),
firecrawl: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
})
.strict()
.optional(),
gemini: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
})
.strict()
.optional(),
grok: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
inlineCitations: z.boolean().optional(),
})
.strict()
.optional(),
kimi: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
})
.strict()
.optional(),
perplexity: z
.object({
apiKey: SecretInputSchema.optional().register(sensitive),
baseUrl: z.string().optional(),
model: z.string().optional(),
})
.strict()
.optional(),
})
.strict()
.optional();

View File

@ -94,14 +94,22 @@ function aspectRatioToEnum(aspectRatio: string | undefined): string | undefined
return undefined;
}
function aspectRatioToDimensions(aspectRatio: string, edge: number): { width: number; height: number } {
function aspectRatioToDimensions(
aspectRatio: string,
edge: number,
): { width: number; height: number } {
const match = /^(\d+):(\d+)$/u.exec(aspectRatio.trim());
if (!match) {
throw new Error(`Invalid fal aspect ratio: ${aspectRatio}`);
}
const widthRatio = Number.parseInt(match[1] ?? "", 10);
const heightRatio = Number.parseInt(match[2] ?? "", 10);
if (!Number.isFinite(widthRatio) || !Number.isFinite(heightRatio) || widthRatio <= 0 || heightRatio <= 0) {
if (
!Number.isFinite(widthRatio) ||
!Number.isFinite(heightRatio) ||
widthRatio <= 0 ||
heightRatio <= 0
) {
throw new Error(`Invalid fal aspect ratio: ${aspectRatio}`);
}
if (widthRatio >= heightRatio) {
@ -140,7 +148,10 @@ function resolveFalImageSize(params: {
return { width: edge, height: edge };
}
if (normalizedAspectRatio) {
return aspectRatioToEnum(normalizedAspectRatio) ?? aspectRatioToDimensions(normalizedAspectRatio, 1024);
return (
aspectRatioToEnum(normalizedAspectRatio) ??
aspectRatioToDimensions(normalizedAspectRatio, 1024)
);
}
return undefined;
}

View File

@ -41,7 +41,7 @@ describe("resolveOutboundSessionRoute", () => {
from?: string;
to?: string;
threadId?: string | number;
chatType?: "direct" | "group";
chatType?: "channel" | "direct" | "group";
};
}> = [
{

View File

@ -972,7 +972,7 @@ describe("resolveOutboundSessionRoute", () => {
from?: string;
to?: string;
threadId?: string | number;
chatType?: "direct" | "group";
chatType?: "channel" | "direct" | "group";
};
}> = [
{

View File

@ -1,6 +1,18 @@
// Public ACP runtime helpers for plugins that integrate with ACP control/session state.
export { getAcpSessionManager } from "../acp/control-plane/manager.js";
export { isAcpRuntimeError } from "../acp/runtime/errors.js";
export { AcpRuntimeError, isAcpRuntimeError } from "../acp/runtime/errors.js";
export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js";
export type {
AcpRuntime,
AcpRuntimeCapabilities,
AcpRuntimeDoctorReport,
AcpRuntimeEnsureInput,
AcpRuntimeEvent,
AcpRuntimeHandle,
AcpRuntimeStatus,
AcpRuntimeTurnInput,
AcpSessionUpdateTag,
} from "../acp/runtime/types.js";
export { readAcpSessionEntry } from "../acp/runtime/session-meta.js";
export type { AcpSessionStoreEntry } from "../acp/runtime/session-meta.js";

View File

@ -41,8 +41,11 @@ export function resolveOptionalConfigString(
}
/** Build the shared allowlist/default target adapter surface for account-scoped channel configs. */
export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
resolveAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => ResolvedAccount;
export function createScopedAccountConfigAccessors<
ResolvedAccount,
Config extends OpenClawConfig = OpenClawConfig,
>(params: {
resolveAccount: (params: { cfg: Config; accountId?: string | null }) => ResolvedAccount;
resolveAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
formatAllowFrom: (allowFrom: Array<string | number>) => string[];
resolveDefaultTo?: (account: ResolvedAccount) => string | number | null | undefined;
@ -52,7 +55,9 @@ export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
> {
const base = {
resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) =>
mapAllowFromEntries(params.resolveAllowFrom(params.resolveAccount({ cfg, accountId }))),
mapAllowFromEntries(
params.resolveAllowFrom(params.resolveAccount({ cfg: cfg as Config, accountId })),
),
formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> }) =>
params.formatAllowFrom(allowFrom),
};
@ -65,7 +70,7 @@ export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
...base,
resolveDefaultTo: ({ cfg, accountId }) =>
resolveOptionalConfigString(
params.resolveDefaultTo?.(params.resolveAccount({ cfg, accountId })),
params.resolveDefaultTo?.(params.resolveAccount({ cfg: cfg as Config, accountId })),
),
};
}
@ -160,7 +165,7 @@ export function createScopedChannelConfigAdapter<
clearBaseFields: params.clearBaseFields,
allowTopLevel: params.allowTopLevel,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
...createScopedAccountConfigAccessors<AccessorAccount, Config>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,
@ -316,7 +321,7 @@ export function createTopLevelChannelConfigAdapter<
deleteMode: params.deleteMode,
clearBaseFields: params.clearBaseFields,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
...createScopedAccountConfigAccessors<AccessorAccount, Config>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,
@ -438,7 +443,7 @@ export function createHybridChannelConfigAdapter<
clearBaseFields: params.clearBaseFields,
preserveSectionOnDefaultDelete: params.preserveSectionOnDefaultDelete,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
...createScopedAccountConfigAccessors<AccessorAccount, Config>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,

View File

@ -44,6 +44,7 @@ export type {
ProviderThinkingPolicyContext,
ProviderWrapStreamFnContext,
OpenClawPluginService,
OpenClawPluginServiceContext,
ProviderAuthContext,
ProviderAuthDoctorHintContext,
ProviderAuthMethodNonInteractiveContext,
@ -51,6 +52,7 @@ export type {
ProviderAuthResult,
OpenClawPluginCommandDefinition,
OpenClawPluginDefinition,
PluginLogger,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
export type { OpenClawConfig } from "../config/config.js";

View File

@ -25,7 +25,7 @@ function collectPluginSdkPackageExports(): string[] {
}
subpaths.push(key.slice("./plugin-sdk/".length));
}
return subpaths.sort();
return subpaths.toSorted();
}
function collectPluginSdkSourceNames(): string[] {
@ -35,7 +35,7 @@ function collectPluginSdkSourceNames(): string[] {
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts"),
)
.map((entry) => entry.name.slice(0, -".ts".length))
.sort();
.toSorted();
}
function collectTextFiles(rootRelativeDir: string): string[] {
@ -92,7 +92,7 @@ function collectPluginSdkSubpathReferences() {
describe("plugin-sdk package contract guardrails", () => {
it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => {
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].sort());
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].toSorted());
});
it("keeps repo openclaw/plugin-sdk/<name> references on exported built subpaths", () => {
@ -135,7 +135,7 @@ describe("plugin-sdk package contract guardrails", () => {
failures.push(
`src/plugin-sdk/${sourceName}.ts is referenced as openclaw/plugin-sdk/${sourceName} in ${matchingRefs
.map((reference) => reference.file)
.sort()
.toSorted()
.join(", ")}, but ${sourceName} is not exported as a public plugin-sdk subpath`,
);
}

View File

@ -26,6 +26,8 @@ export type { StickerMetadata } from "../../extensions/telegram/api.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export { parseTelegramTopicConversation } from "../acp/conversation-id.js";
export { clearAccountEntryFields } from "../channels/plugins/config-helpers.js";
export { resolveTelegramPollVisibility } from "../poll-params.js";
export {
PAIRING_APPROVED_MESSAGE,
@ -38,9 +40,6 @@ export {
setAccountEnabledInConfigSection,
} from "./channel-plugin-common.js";
export { clearAccountEntryFields } from "../channels/plugins/config-helpers.js";
export { resolveTelegramPollVisibility } from "../poll-params.js";
export {
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,

View File

@ -99,6 +99,7 @@ describe("plugin shape compatibility matrix", () => {
envVars: ["HYBRID_SEARCH_KEY"],
placeholder: "hsk_...",
signupUrl: "https://example.com/signup",
credentialPath: "tools.web.search.hybrid-search.apiKey",
getCredentialValue: () => "hsk-test",
setCredentialValue(searchConfigTarget, value) {
searchConfigTarget.apiKey = value;

View File

@ -68,7 +68,10 @@ function createProviderSecretRefConfig(
}
function readProviderKey(config: OpenClawConfig, provider: ProviderUnderTest): unknown {
return config.plugins?.entries?.[providerPluginId(provider)]?.config?.webSearch?.apiKey;
const pluginConfig = config.plugins?.entries?.[providerPluginId(provider)]?.config as
| { webSearch?: { apiKey?: unknown } }
| undefined;
return pluginConfig?.webSearch?.apiKey;
}
function expectInactiveFirecrawlSecretRef(params: {

View File

@ -21,6 +21,7 @@ describe("web search runtime", () => {
placeholder: "custom-...",
signupUrl: "https://example.com/signup",
autoDetectOrder: 1,
credentialPath: "tools.web.search.custom.apiKey",
getCredentialValue: () => "configured",
setCredentialValue: () => {},
createTool: () => ({

View File

@ -199,5 +199,6 @@ export async function runWebSearch(
export const __testing = {
resolveSearchConfig,
resolveSearchProvider: resolveWebSearchProviderId,
resolveWebSearchProviderId,
};

View File

@ -42,6 +42,8 @@ describe("config view", () => {
themeMode: "system" as ThemeMode,
setTheme: vi.fn(),
setThemeMode: vi.fn(),
borderRadius: 50,
setBorderRadius: vi.fn(),
gatewayUrl: "",
assistantName: "OpenClaw",
});