From 07d9f725b618bd676b791f6d1949ecb2bff759c1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Mar 2026 23:58:49 +0000 Subject: [PATCH] refactor: unify plugin sdk primitives --- docs/plugins/architecture.md | 9 ++ docs/plugins/building-extensions.md | 33 +++-- extensions/bluebubbles/src/secret-input.ts | 8 +- extensions/chutes/onboard.ts | 28 ++--- .../src/monitor/message-handler.process.ts | 37 +++--- extensions/feishu/src/bot.ts | 9 +- extensions/feishu/src/secret-input.ts | 9 +- extensions/googlechat/src/monitor-access.ts | 9 +- extensions/googlechat/src/monitor.ts | 6 +- extensions/huggingface/onboard.ts | 27 ++--- extensions/irc/src/inbound.ts | 9 +- extensions/kimi-coding/onboard.ts | 29 ++--- extensions/matrix/src/secret-input.ts | 9 +- extensions/mattermost/src/secret-input.ts | 9 +- extensions/mistral/onboard.ts | 24 ++-- extensions/modelstudio/onboard.ts | 34 +++--- extensions/moonshot/onboard.ts | 25 ++-- extensions/nextcloud-talk/src/inbound.ts | 9 +- extensions/nextcloud-talk/src/secret-input.ts | 9 +- extensions/opencode-go/onboard.ts | 17 ++- extensions/opencode/onboard.ts | 11 +- extensions/qianfan/onboard.ts | 45 ++++--- .../src/monitor/message-handler/dispatch.ts | 113 +++++++++--------- extensions/synthetic/onboard.ts | 27 ++--- .../telegram/src/bot-message-dispatch.ts | 36 +++--- extensions/together/onboard.ts | 27 ++--- extensions/venice/onboard.ts | 24 ++-- extensions/xai/onboard.ts | 17 +-- extensions/zai/onboard.ts | 52 ++++---- extensions/zalo/src/monitor.ts | 59 +++++---- extensions/zalo/src/secret-input.ts | 9 +- extensions/zalouser/src/monitor.ts | 43 +++---- package.json | 20 ++++ scripts/lib/plugin-sdk-entrypoints.json | 5 + .../onboard-auth.config-shared.test.ts | 75 ++++++++++++ src/plugin-sdk/channel-pairing.test.ts | 48 ++++++++ src/plugin-sdk/channel-pairing.ts | 31 +++++ src/plugin-sdk/channel-reply-pipeline.test.ts | 39 ++++++ src/plugin-sdk/channel-reply-pipeline.ts | 38 ++++++ src/plugin-sdk/channel-setup.test.ts | 38 ++++++ src/plugin-sdk/channel-setup.ts | 42 +++++++ src/plugin-sdk/feishu.ts | 17 ++- src/plugin-sdk/googlechat.ts | 30 ++--- src/plugin-sdk/irc.ts | 5 +- src/plugin-sdk/matrix.ts | 27 ++--- src/plugin-sdk/msteams.ts | 21 ++-- src/plugin-sdk/nextcloud-talk.ts | 11 +- src/plugin-sdk/nostr.ts | 15 +-- src/plugin-sdk/provider-onboard.ts | 5 + src/plugin-sdk/secret-input.test.ts | 24 ++++ src/plugin-sdk/secret-input.ts | 23 ++++ src/plugin-sdk/subpaths.test.ts | 32 +++++ src/plugin-sdk/tlon.ts | 17 +-- src/plugin-sdk/twitch.ts | 16 +-- src/plugin-sdk/webhook-ingress.ts | 38 ++++++ src/plugin-sdk/zalo.ts | 39 +++--- src/plugin-sdk/zalouser.ts | 22 ++-- src/plugins/provider-onboarding-config.ts | 105 ++++++++++++++++ 58 files changed, 1007 insertions(+), 588 deletions(-) create mode 100644 src/plugin-sdk/channel-pairing.test.ts create mode 100644 src/plugin-sdk/channel-pairing.ts create mode 100644 src/plugin-sdk/channel-reply-pipeline.test.ts create mode 100644 src/plugin-sdk/channel-reply-pipeline.ts create mode 100644 src/plugin-sdk/channel-setup.test.ts create mode 100644 src/plugin-sdk/channel-setup.ts create mode 100644 src/plugin-sdk/secret-input.test.ts create mode 100644 src/plugin-sdk/secret-input.ts create mode 100644 src/plugin-sdk/webhook-ingress.ts diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 1a130085773..f857b8f1b1c 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -925,6 +925,12 @@ authoring plugins: - `openclaw/plugin-sdk/plugin-entry` for plugin registration primitives. - `openclaw/plugin-sdk/core` for the generic shared plugin-facing contract. +- Stable channel primitives such as `openclaw/plugin-sdk/channel-setup`, + `openclaw/plugin-sdk/channel-pairing`, + `openclaw/plugin-sdk/channel-reply-pipeline`, + `openclaw/plugin-sdk/secret-input`, and + `openclaw/plugin-sdk/webhook-ingress` for shared setup/auth/reply/webhook + wiring. - Domain subpaths such as `openclaw/plugin-sdk/channel-config-helpers`, `openclaw/plugin-sdk/channel-config-schema`, `openclaw/plugin-sdk/channel-policy`, @@ -961,6 +967,9 @@ authoring plugins: Compatibility note: - Avoid the root `openclaw/plugin-sdk` barrel for new code. +- Prefer the narrow stable primitives first. The newer setup/pairing/reply/ + secret-input/webhook subpaths are the intended contract for new bundled and + external plugin work. - Bundled extension-specific helper barrels are not stable by default. If a helper is only needed by a bundled extension, keep it behind the extension's local `api.js` or `runtime-api.js` seam instead of promoting it into diff --git a/docs/plugins/building-extensions.md b/docs/plugins/building-extensions.md index dc9bc9ea829..259accaa3f0 100644 --- a/docs/plugins/building-extensions.md +++ b/docs/plugins/building-extensions.md @@ -95,8 +95,10 @@ subpaths rather than the monolithic root: ```typescript // Correct: focused subpaths import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; -import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime"; +import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; +import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing"; import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; +import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup"; import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy"; // Wrong: monolithic root (lint will reject this) @@ -105,17 +107,24 @@ import { ... } from "openclaw/plugin-sdk"; Common subpaths: -| Subpath | Purpose | -| ---------------------------------- | ------------------------------------ | -| `plugin-sdk/core` | Plugin entry definitions, base types | -| `plugin-sdk/channel-runtime` | Channel runtime helpers | -| `plugin-sdk/channel-config-schema` | Config schema builders | -| `plugin-sdk/channel-policy` | Group/DM policy helpers | -| `plugin-sdk/setup` | Setup wizard adapters | -| `plugin-sdk/runtime-store` | Persistent plugin storage | -| `plugin-sdk/allow-from` | Allowlist resolution | -| `plugin-sdk/reply-payload` | Message reply types | -| `plugin-sdk/testing` | Test utilities | +| Subpath | Purpose | +| ----------------------------------- | ------------------------------------ | +| `plugin-sdk/core` | Plugin entry definitions, base types | +| `plugin-sdk/channel-setup` | Optional setup adapters/wizards | +| `plugin-sdk/channel-pairing` | DM pairing primitives | +| `plugin-sdk/channel-reply-pipeline` | Prefix + typing reply wiring | +| `plugin-sdk/channel-config-schema` | Config schema builders | +| `plugin-sdk/channel-policy` | Group/DM policy helpers | +| `plugin-sdk/secret-input` | Secret input parsing/helpers | +| `plugin-sdk/webhook-ingress` | Webhook request/target helpers | +| `plugin-sdk/runtime-store` | Persistent plugin storage | +| `plugin-sdk/allow-from` | Allowlist resolution | +| `plugin-sdk/reply-payload` | Message reply types | +| `plugin-sdk/provider-onboard` | Provider onboarding config patches | +| `plugin-sdk/testing` | Test utilities | + +Use the narrowest primitive that matches the job. Reach for `channel-runtime` +or other larger helper barrels only when a dedicated subpath does not exist yet. ## Step 4: Use local barrels for internal imports diff --git a/extensions/bluebubbles/src/secret-input.ts b/extensions/bluebubbles/src/secret-input.ts index b0386988c42..f1b2aae5c92 100644 --- a/extensions/bluebubbles/src/secret-input.ts +++ b/extensions/bluebubbles/src/secret-input.ts @@ -1,12 +1,6 @@ -import { - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "openclaw/plugin-sdk/secret-input-runtime"; -import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input-schema"; export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/chutes/onboard.ts b/extensions/chutes/onboard.ts index f51914c3ca8..a41b3689122 100644 --- a/extensions/chutes/onboard.ts +++ b/extensions/chutes/onboard.ts @@ -6,7 +6,7 @@ import { } from "openclaw/plugin-sdk/provider-models"; import { applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; @@ -17,24 +17,20 @@ export { CHUTES_DEFAULT_MODEL_REF }; * Registers all catalog models and sets provider aliases (chutes-fast, etc.). */ export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - for (const m of CHUTES_MODEL_CATALOG) { - models[`chutes/${m.id}`] = { - ...models[`chutes/${m.id}`], - }; - } - - models["chutes-fast"] = { alias: "chutes/zai-org/GLM-4.7-FP8" }; - models["chutes-vision"] = { alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506" }; - models["chutes-pro"] = { alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" }; - - const chutesModels = CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition); - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "chutes", api: "openai-completions", baseUrl: CHUTES_BASE_URL, - catalogModels: chutesModels, + catalogModels: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition), + aliases: [ + ...CHUTES_MODEL_CATALOG.map((model) => `chutes/${model.id}`), + { modelRef: "chutes-fast", alias: "chutes/zai-org/GLM-4.7-FP8" }, + { + modelRef: "chutes-vision", + alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506", + }, + { modelRef: "chutes-pro", alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" }, + ], }); } diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index f24a9e27774..42f2011d62a 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -1,16 +1,15 @@ import { ChannelType, type RequestClient } from "@buape/carbon"; import { resolveAckReaction, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime"; import { EmbeddedBlockChunker } from "openclaw/plugin-sdk/agent-runtime"; +import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; import { shouldAckReaction as shouldAckReactionGate } from "openclaw/plugin-sdk/channel-runtime"; import { logTypingFailure, logAckFailure } from "openclaw/plugin-sdk/channel-runtime"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime"; import { recordInboundSession } from "openclaw/plugin-sdk/channel-runtime"; import { createStatusReactionController, DEFAULT_TIMING, type StatusReactionAdapter, } from "openclaw/plugin-sdk/channel-runtime"; -import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime"; import { resolveDiscordPreviewStreamMode } from "openclaw/plugin-sdk/config-runtime"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; @@ -420,11 +419,24 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) ? deliverTarget.slice("channel:".length) : messageChannelId; - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: route.agentId, channel: "discord", accountId: route.accountId, + typing: { + start: () => sendTyping({ client, channelId: typingChannelId }), + onStartError: (err) => { + logTypingFailure({ + log: logVerbose, + channel: "discord", + target: typingChannelId, + error: err, + }); + }, + // Long tool-heavy runs are expected on Discord; keep heartbeats alive. + maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS, + }, }); const tableMode = resolveMarkdownTableMode({ cfg, @@ -438,20 +450,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) }); const chunkMode = resolveChunkMode(cfg, "discord", accountId); - const typingCallbacks = createTypingCallbacks({ - start: () => sendTyping({ client, channelId: typingChannelId }), - onStartError: (err) => { - logTypingFailure({ - log: logVerbose, - channel: "discord", - target: typingChannelId, - error: err, - }); - }, - // Long tool-heavy runs are expected on Discord; keep heartbeats alive. - maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS, - }); - // --- Discord draft stream (edit-based preview streaming) --- const discordStreamMode = resolveDiscordPreviewStreamMode(discordConfig); const draftMaxChars = Math.min(textLimit, 2000); @@ -597,9 +595,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), - typingCallbacks, deliver: async (payload: ReplyPayload, info) => { if (isProcessAborted(abortSignal)) { return; @@ -715,7 +712,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) if (isProcessAborted(abortSignal)) { return; } - await typingCallbacks.onReplyStart(); + await replyPipeline.typingCallbacks?.onReplyStart(); await statusReactions.setThinking(); }, }); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 3a7e62adc68..63b898a23fb 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -10,10 +10,9 @@ import { buildAgentMediaPayload, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, - createScopedPairingAccess, + createChannelPairingController, DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry, - issuePairingChallenge, normalizeAgentId, recordPendingHistoryEntryIfEnabled, resolveAgentOutboundIdentity, @@ -445,7 +444,7 @@ export async function handleFeishuMessage(params: { try { const core = getFeishuRuntime(); - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "feishu", accountId: account.accountId, @@ -471,12 +470,10 @@ export async function handleFeishuMessage(params: { if (isDirect && dmPolicy !== "open" && !dmAllowed) { if (dmPolicy === "pairing") { - await issuePairingChallenge({ - channel: "feishu", + await pairing.issueChallenge({ senderId: ctx.senderOpenId, senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`, meta: { name: ctx.senderName }, - upsertPairingRequest: pairing.upsertPairingRequest, onCreated: () => { log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`); }, diff --git a/extensions/feishu/src/secret-input.ts b/extensions/feishu/src/secret-input.ts index ad5746ffc31..f1b2aae5c92 100644 --- a/extensions/feishu/src/secret-input.ts +++ b/extensions/feishu/src/secret-input.ts @@ -1,13 +1,6 @@ -import { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "../runtime-api.js"; - export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/googlechat/src/monitor-access.ts b/extensions/googlechat/src/monitor-access.ts index 8bc5315b635..e9edb7eb67e 100644 --- a/extensions/googlechat/src/monitor-access.ts +++ b/extensions/googlechat/src/monitor-access.ts @@ -1,8 +1,7 @@ import { GROUP_POLICY_BLOCKED_LABEL, - createScopedPairingAccess, + createChannelPairingController, evaluateGroupRouteAccessForPolicy, - issuePairingChallenge, isDangerousNameMatchingEnabled, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, @@ -166,7 +165,7 @@ export async function applyGoogleChatInboundAccessPolicy(params: { } = params; const allowNameMatching = isDangerousNameMatchingEnabled(account.config); const spaceId = space.name ?? ""; - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "googlechat", accountId: account.accountId, @@ -311,12 +310,10 @@ export async function applyGoogleChatInboundAccessPolicy(params: { if (access.decision !== "allow") { if (access.decision === "pairing") { - await issuePairingChallenge({ - channel: "googlechat", + await pairing.issueChallenge({ senderId, senderIdLine: `Your Google Chat user id: ${senderId}`, meta: { name: senderName || undefined, email: senderEmail }, - upsertPairingRequest: pairing.upsertPairingRequest, onCreated: () => { logVerbose(`googlechat pairing request sender=${senderId}`); }, diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index b0612842919..49621420e13 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -5,8 +5,8 @@ import { } from "openclaw/plugin-sdk/reply-payload"; import type { OpenClawConfig } from "../runtime-api.js"; import { + createChannelReplyPipeline, createWebhookInFlightLimiter, - createReplyPrefixOptions, registerWebhookTargetWithPluginRoute, resolveInboundRouteEnvelopeBuilderWithRuntime, resolveWebhookPath, @@ -307,7 +307,7 @@ async function processMessageWithPipeline(params: { } } - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg: config, agentId: route.agentId, channel: "googlechat", @@ -318,7 +318,7 @@ async function processMessageWithPipeline(params: { ctx: ctxPayload, cfg: config, dispatcherOptions: { - ...prefixOptions, + ...replyPipeline, deliver: async (payload) => { await deliverGoogleChatReply({ payload, diff --git a/extensions/huggingface/onboard.ts b/extensions/huggingface/onboard.ts index 40df946abe3..e8f7412768c 100644 --- a/extensions/huggingface/onboard.ts +++ b/extensions/huggingface/onboard.ts @@ -4,32 +4,27 @@ import { HUGGINGFACE_MODEL_CATALOG, } from "openclaw/plugin-sdk/provider-models"; import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1"; -export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[HUGGINGFACE_DEFAULT_MODEL_REF] = { - ...models[HUGGINGFACE_DEFAULT_MODEL_REF], - alias: models[HUGGINGFACE_DEFAULT_MODEL_REF]?.alias ?? "Hugging Face", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, +function applyHuggingfacePreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "huggingface", api: "openai-completions", baseUrl: HUGGINGFACE_BASE_URL, catalogModels: HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition), + aliases: [{ modelRef: HUGGINGFACE_DEFAULT_MODEL_REF, alias: "Hugging Face" }], + primaryModelRef, }); } -export function applyHuggingfaceConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyHuggingfaceProviderConfig(cfg), - HUGGINGFACE_DEFAULT_MODEL_REF, - ); +export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyHuggingfacePreset(cfg); +} + +export function applyHuggingfaceConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyHuggingfacePreset(cfg, HUGGINGFACE_DEFAULT_MODEL_REF); } diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index aa763d4c561..56067d4c35d 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -9,10 +9,9 @@ import { } from "./policy.js"; import { GROUP_POLICY_BLOCKED_LABEL, - createScopedPairingAccess, + createChannelPairingController, deliverFormattedTextWithAttachments, dispatchInboundReplyWithBase, - issuePairingChallenge, logInboundDrop, isDangerousNameMatchingEnabled, readStoreAllowFromForDmPolicy, @@ -90,7 +89,7 @@ export async function handleIrcInbound(params: { }): Promise { const { message, account, config, runtime, connectedNick, statusSink } = params; const core = getIrcRuntime(); - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: CHANNEL_ID, accountId: account.accountId, @@ -208,12 +207,10 @@ export async function handleIrcInbound(params: { }).allowed; if (!dmAllowed) { if (dmPolicy === "pairing") { - await issuePairingChallenge({ - channel: CHANNEL_ID, + await pairing.issueChallenge({ senderId: senderDisplay.toLowerCase(), senderIdLine: `Your IRC id: ${senderDisplay}`, meta: { name: message.senderNick || undefined }, - upsertPairingRequest: pairing.upsertPairingRequest, sendPairingReply: async (text) => { await deliverIrcReply({ payload: { text }, diff --git a/extensions/kimi-coding/onboard.ts b/extensions/kimi-coding/onboard.ts index 60ce12553f1..65d2e7aabe7 100644 --- a/extensions/kimi-coding/onboard.ts +++ b/extensions/kimi-coding/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModel, + applyProviderConfigWithDefaultModelPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { @@ -12,28 +11,30 @@ import { export const KIMI_MODEL_REF = `kimi/${KIMI_CODING_DEFAULT_MODEL_ID}`; export const KIMI_CODING_MODEL_REF = KIMI_MODEL_REF; -export function applyKimiCodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[KIMI_MODEL_REF] = { - ...models[KIMI_MODEL_REF], - alias: models[KIMI_MODEL_REF]?.alias ?? "Kimi", - }; +function resolveKimiCodingDefaultModel() { + return buildKimiCodingProvider().models[0]; +} - const defaultModel = buildKimiCodingProvider().models[0]; +function applyKimiCodingPreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + const defaultModel = resolveKimiCodingDefaultModel(); if (!defaultModel) { return cfg; } - - return applyProviderConfigWithDefaultModel(cfg, { - agentModels: models, + return applyProviderConfigWithDefaultModelPreset(cfg, { providerId: "kimi", api: "anthropic-messages", baseUrl: KIMI_CODING_BASE_URL, defaultModel, defaultModelId: KIMI_CODING_DEFAULT_MODEL_ID, + aliases: [{ modelRef: KIMI_MODEL_REF, alias: "Kimi" }], + primaryModelRef, }); } -export function applyKimiCodeConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyKimiCodeProviderConfig(cfg), KIMI_MODEL_REF); +export function applyKimiCodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyKimiCodingPreset(cfg); +} + +export function applyKimiCodeConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyKimiCodingPreset(cfg, KIMI_MODEL_REF); } diff --git a/extensions/matrix/src/secret-input.ts b/extensions/matrix/src/secret-input.ts index ad5746ffc31..f1b2aae5c92 100644 --- a/extensions/matrix/src/secret-input.ts +++ b/extensions/matrix/src/secret-input.ts @@ -1,13 +1,6 @@ -import { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "../runtime-api.js"; - export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/mattermost/src/secret-input.ts b/extensions/mattermost/src/secret-input.ts index b32083456e7..f1b2aae5c92 100644 --- a/extensions/mattermost/src/secret-input.ts +++ b/extensions/mattermost/src/secret-input.ts @@ -1,13 +1,6 @@ -import { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "./runtime-api.js"; - export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/mistral/onboard.ts b/extensions/mistral/onboard.ts index 337ef194f1c..02093d6a9bb 100644 --- a/extensions/mistral/onboard.ts +++ b/extensions/mistral/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModel, + applyProviderConfigWithDefaultModelPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { @@ -11,23 +10,22 @@ import { export const MISTRAL_DEFAULT_MODEL_REF = `mistral/${MISTRAL_DEFAULT_MODEL_ID}`; -export function applyMistralProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[MISTRAL_DEFAULT_MODEL_REF] = { - ...models[MISTRAL_DEFAULT_MODEL_REF], - alias: models[MISTRAL_DEFAULT_MODEL_REF]?.alias ?? "Mistral", - }; - - return applyProviderConfigWithDefaultModel(cfg, { - agentModels: models, +function applyMistralPreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + return applyProviderConfigWithDefaultModelPreset(cfg, { providerId: "mistral", api: "openai-completions", baseUrl: MISTRAL_BASE_URL, defaultModel: buildMistralModelDefinition(), defaultModelId: MISTRAL_DEFAULT_MODEL_ID, + aliases: [{ modelRef: MISTRAL_DEFAULT_MODEL_REF, alias: "Mistral" }], + primaryModelRef, }); } -export function applyMistralConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyMistralProviderConfig(cfg), MISTRAL_DEFAULT_MODEL_REF); +export function applyMistralProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyMistralPreset(cfg); +} + +export function applyMistralConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyMistralPreset(cfg, MISTRAL_DEFAULT_MODEL_REF); } diff --git a/extensions/modelstudio/onboard.ts b/extensions/modelstudio/onboard.ts index 9c1d78a141b..5252915bf25 100644 --- a/extensions/modelstudio/onboard.ts +++ b/extensions/modelstudio/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { @@ -15,26 +14,19 @@ export { MODELSTUDIO_CN_BASE_URL, MODELSTUDIO_DEFAULT_MODEL_REF, MODELSTUDIO_GLO function applyModelStudioProviderConfigWithBaseUrl( cfg: OpenClawConfig, baseUrl: string, + primaryModelRef?: string, ): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; const provider = buildModelStudioProvider(); - for (const model of provider.models ?? []) { - const modelRef = `modelstudio/${model.id}`; - if (!models[modelRef]) { - models[modelRef] = {}; - } - } - models[MODELSTUDIO_DEFAULT_MODEL_REF] = { - ...models[MODELSTUDIO_DEFAULT_MODEL_REF], - alias: models[MODELSTUDIO_DEFAULT_MODEL_REF]?.alias ?? "Qwen", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "modelstudio", api: provider.api ?? "openai-completions", baseUrl, catalogModels: provider.models ?? [], + aliases: [ + ...(provider.models ?? []).map((model) => `modelstudio/${model.id}`), + { modelRef: MODELSTUDIO_DEFAULT_MODEL_REF, alias: "Qwen" }, + ], + primaryModelRef, }); } @@ -47,15 +39,17 @@ export function applyModelStudioProviderConfigCn(cfg: OpenClawConfig): OpenClawC } export function applyModelStudioConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyModelStudioProviderConfig(cfg), + return applyModelStudioProviderConfigWithBaseUrl( + cfg, + MODELSTUDIO_GLOBAL_BASE_URL, MODELSTUDIO_DEFAULT_MODEL_REF, ); } export function applyModelStudioConfigCn(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyModelStudioProviderConfigCn(cfg), + return applyModelStudioProviderConfigWithBaseUrl( + cfg, + MODELSTUDIO_CN_BASE_URL, MODELSTUDIO_DEFAULT_MODEL_REF, ); } diff --git a/extensions/moonshot/onboard.ts b/extensions/moonshot/onboard.ts index 61cc537a622..a4e937b3df5 100644 --- a/extensions/moonshot/onboard.ts +++ b/extensions/moonshot/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModel, + applyProviderConfigWithDefaultModelPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { @@ -23,38 +22,32 @@ export function applyMoonshotProviderConfigCn(cfg: OpenClawConfig): OpenClawConf function applyMoonshotProviderConfigWithBaseUrl( cfg: OpenClawConfig, baseUrl: string, + primaryModelRef?: string, ): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[MOONSHOT_DEFAULT_MODEL_REF] = { - ...models[MOONSHOT_DEFAULT_MODEL_REF], - alias: models[MOONSHOT_DEFAULT_MODEL_REF]?.alias ?? "Kimi", - }; - const defaultModel = buildMoonshotProvider().models[0]; if (!defaultModel) { return cfg; } - return applyProviderConfigWithDefaultModel(cfg, { - agentModels: models, + return applyProviderConfigWithDefaultModelPreset(cfg, { providerId: "moonshot", api: "openai-completions", baseUrl, defaultModel, defaultModelId: MOONSHOT_DEFAULT_MODEL_ID, + aliases: [{ modelRef: MOONSHOT_DEFAULT_MODEL_REF, alias: "Kimi" }], + primaryModelRef, }); } export function applyMoonshotConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyMoonshotProviderConfig(cfg), - MOONSHOT_DEFAULT_MODEL_REF, - ); + return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_BASE_URL, MOONSHOT_DEFAULT_MODEL_REF); } export function applyMoonshotConfigCn(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyMoonshotProviderConfigCn(cfg), + return applyMoonshotProviderConfigWithBaseUrl( + cfg, + MOONSHOT_CN_BASE_URL, MOONSHOT_DEFAULT_MODEL_REF, ); } diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index d9f4de2f9a2..c5220837c6d 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -1,9 +1,8 @@ import { GROUP_POLICY_BLOCKED_LABEL, - createScopedPairingAccess, + createChannelPairingController, deliverFormattedTextWithAttachments, dispatchInboundReplyWithBase, - issuePairingChallenge, logInboundDrop, readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithCommandGate, @@ -58,7 +57,7 @@ export async function handleNextcloudTalkInbound(params: { }): Promise { const { message, account, config, runtime, statusSink } = params; const core = getNextcloudTalkRuntime(); - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: CHANNEL_ID, accountId: account.accountId, @@ -172,12 +171,10 @@ export async function handleNextcloudTalkInbound(params: { } else { if (access.decision !== "allow") { if (access.decision === "pairing") { - await issuePairingChallenge({ - channel: CHANNEL_ID, + await pairing.issueChallenge({ senderId, senderIdLine: `Your Nextcloud user id: ${senderId}`, meta: { name: senderName || undefined }, - upsertPairingRequest: pairing.upsertPairingRequest, sendPairingReply: async (text) => { await sendMessageNextcloudTalk(roomToken, text, { accountId: account.accountId }); statusSink?.({ lastOutboundAt: Date.now() }); diff --git a/extensions/nextcloud-talk/src/secret-input.ts b/extensions/nextcloud-talk/src/secret-input.ts index ad5746ffc31..f1b2aae5c92 100644 --- a/extensions/nextcloud-talk/src/secret-input.ts +++ b/extensions/nextcloud-talk/src/secret-input.ts @@ -1,13 +1,6 @@ -import { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "../runtime-api.js"; - export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/opencode-go/onboard.ts b/extensions/opencode-go/onboard.ts index ec5727f9525..2895ff4c5a4 100644 --- a/extensions/opencode-go/onboard.ts +++ b/extensions/opencode-go/onboard.ts @@ -1,6 +1,7 @@ import { OPENCODE_GO_DEFAULT_MODEL_REF } from "openclaw/plugin-sdk/provider-models"; import { applyAgentDefaultModelPrimary, + withAgentModelAliases, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; @@ -13,21 +14,19 @@ const OPENCODE_GO_ALIAS_DEFAULTS: Record = { }; export function applyOpencodeGoProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - for (const [modelRef, alias] of Object.entries(OPENCODE_GO_ALIAS_DEFAULTS)) { - models[modelRef] = { - ...models[modelRef], - alias: models[modelRef]?.alias ?? alias, - }; - } - return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, - models, + models: withAgentModelAliases( + cfg.agents?.defaults?.models, + Object.entries(OPENCODE_GO_ALIAS_DEFAULTS).map(([modelRef, alias]) => ({ + modelRef, + alias, + })), + ), }, }, }; diff --git a/extensions/opencode/onboard.ts b/extensions/opencode/onboard.ts index 5bccbb34d8a..4a85ff74348 100644 --- a/extensions/opencode/onboard.ts +++ b/extensions/opencode/onboard.ts @@ -1,25 +1,22 @@ import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "openclaw/plugin-sdk/provider-models"; import { applyAgentDefaultModelPrimary, + withAgentModelAliases, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; export { OPENCODE_ZEN_DEFAULT_MODEL_REF }; export function applyOpencodeZenProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[OPENCODE_ZEN_DEFAULT_MODEL_REF] = { - ...models[OPENCODE_ZEN_DEFAULT_MODEL_REF], - alias: models[OPENCODE_ZEN_DEFAULT_MODEL_REF]?.alias ?? "Opus", - }; - return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, - models, + models: withAgentModelAliases(cfg.agents?.defaults?.models, [ + { modelRef: OPENCODE_ZEN_DEFAULT_MODEL_REF, alias: "Opus" }, + ]), }, }, }; diff --git a/extensions/qianfan/onboard.ts b/extensions/qianfan/onboard.ts index c389868c7d8..0485c8b9676 100644 --- a/extensions/qianfan/onboard.ts +++ b/extensions/qianfan/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModels, + applyProviderConfigWithDefaultModelsPreset, type ModelApi, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; @@ -12,12 +11,11 @@ import { export const QIANFAN_DEFAULT_MODEL_REF = `qianfan/${QIANFAN_DEFAULT_MODEL_ID}`; -export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[QIANFAN_DEFAULT_MODEL_REF] = { - ...models[QIANFAN_DEFAULT_MODEL_REF], - alias: models[QIANFAN_DEFAULT_MODEL_REF]?.alias ?? "QIANFAN", - }; +function resolveQianfanPreset(cfg: OpenClawConfig): { + api: ModelApi; + baseUrl: string; + defaultModels: NonNullable["models"]>; +} { const defaultProvider = buildQianfanProvider(); const existingProvider = cfg.models?.providers?.qianfan as | { @@ -27,22 +25,35 @@ export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig | undefined; const existingBaseUrl = typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; - const resolvedBaseUrl = existingBaseUrl || QIANFAN_BASE_URL; - const resolvedApi = + const api = typeof existingProvider?.api === "string" ? (existingProvider.api as ModelApi) : "openai-completions"; - return applyProviderConfigWithDefaultModels(cfg, { - agentModels: models, - providerId: "qianfan", - api: resolvedApi, - baseUrl: resolvedBaseUrl, + return { + api, + baseUrl: existingBaseUrl || QIANFAN_BASE_URL, defaultModels: defaultProvider.models ?? [], + }; +} + +function applyQianfanPreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + const preset = resolveQianfanPreset(cfg); + return applyProviderConfigWithDefaultModelsPreset(cfg, { + providerId: "qianfan", + api: preset.api, + baseUrl: preset.baseUrl, + defaultModels: preset.defaultModels, defaultModelId: QIANFAN_DEFAULT_MODEL_ID, + aliases: [{ modelRef: QIANFAN_DEFAULT_MODEL_REF, alias: "QIANFAN" }], + primaryModelRef, }); } -export function applyQianfanConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyQianfanProviderConfig(cfg), QIANFAN_DEFAULT_MODEL_REF); +export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyQianfanPreset(cfg); +} + +export function applyQianfanConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyQianfanPreset(cfg, QIANFAN_DEFAULT_MODEL_REF); } diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 5fac27f002b..2b31791284e 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -1,8 +1,7 @@ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime"; +import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; import { removeAckReactionAfterReply } from "openclaw/plugin-sdk/channel-runtime"; import { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-runtime"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime"; -import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime"; import { resolveStorePath, updateLastRoute } from "openclaw/plugin-sdk/config-runtime"; import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/infra-runtime"; import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; @@ -147,63 +146,62 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag const typingTarget = statusThreadTs ? `${message.channel}/${statusThreadTs}` : message.channel; const typingReaction = ctx.typingReaction; - const typingCallbacks = createTypingCallbacks({ - start: async () => { - didSetStatus = true; - await ctx.setSlackThreadStatus({ - channelId: message.channel, - threadTs: statusThreadTs, - status: "is typing...", - }); - if (typingReaction && message.ts) { - await reactSlackMessage(message.channel, message.ts, typingReaction, { - token: ctx.botToken, - client: ctx.app.client, - }).catch(() => {}); - } - }, - stop: async () => { - if (!didSetStatus) { - return; - } - didSetStatus = false; - await ctx.setSlackThreadStatus({ - channelId: message.channel, - threadTs: statusThreadTs, - status: "", - }); - if (typingReaction && message.ts) { - await removeSlackReaction(message.channel, message.ts, typingReaction, { - token: ctx.botToken, - client: ctx.app.client, - }).catch(() => {}); - } - }, - onStartError: (err) => { - logTypingFailure({ - log: (message) => runtime.error?.(danger(message)), - channel: "slack", - action: "start", - target: typingTarget, - error: err, - }); - }, - onStopError: (err) => { - logTypingFailure({ - log: (message) => runtime.error?.(danger(message)), - channel: "slack", - action: "stop", - target: typingTarget, - error: err, - }); - }, - }); - - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: route.agentId, channel: "slack", accountId: route.accountId, + typing: { + start: async () => { + didSetStatus = true; + await ctx.setSlackThreadStatus({ + channelId: message.channel, + threadTs: statusThreadTs, + status: "is typing...", + }); + if (typingReaction && message.ts) { + await reactSlackMessage(message.channel, message.ts, typingReaction, { + token: ctx.botToken, + client: ctx.app.client, + }).catch(() => {}); + } + }, + stop: async () => { + if (!didSetStatus) { + return; + } + didSetStatus = false; + await ctx.setSlackThreadStatus({ + channelId: message.channel, + threadTs: statusThreadTs, + status: "", + }); + if (typingReaction && message.ts) { + await removeSlackReaction(message.channel, message.ts, typingReaction, { + token: ctx.botToken, + client: ctx.app.client, + }).catch(() => {}); + } + }, + onStartError: (err) => { + logTypingFailure({ + log: (message) => runtime.error?.(danger(message)), + channel: "slack", + action: "start", + target: typingTarget, + error: err, + }); + }, + onStopError: (err) => { + logTypingFailure({ + log: (message) => runtime.error?.(danger(message)), + channel: "slack", + action: "stop", + target: typingTarget, + error: err, + }); + }, + }, }); const slackStreaming = resolveSlackStreamingConfig({ @@ -299,9 +297,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }; const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), - typingCallbacks, deliver: async (payload) => { if (useStreaming) { await deliverWithStreaming(payload); @@ -367,7 +364,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }, onError: (err, info) => { runtime.error?.(danger(`slack ${info.kind} reply failed: ${String(err)}`)); - typingCallbacks.onIdle?.(); + replyPipeline.typingCallbacks?.onIdle?.(); }, }); diff --git a/extensions/synthetic/onboard.ts b/extensions/synthetic/onboard.ts index d11f2cb0e9b..feae2c312d9 100644 --- a/extensions/synthetic/onboard.ts +++ b/extensions/synthetic/onboard.ts @@ -5,32 +5,27 @@ import { SYNTHETIC_MODEL_CATALOG, } from "openclaw/plugin-sdk/provider-models"; import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; export { SYNTHETIC_DEFAULT_MODEL_REF }; -export function applySyntheticProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[SYNTHETIC_DEFAULT_MODEL_REF] = { - ...models[SYNTHETIC_DEFAULT_MODEL_REF], - alias: models[SYNTHETIC_DEFAULT_MODEL_REF]?.alias ?? "MiniMax M2.5", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, +function applySyntheticPreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "synthetic", api: "anthropic-messages", baseUrl: SYNTHETIC_BASE_URL, catalogModels: SYNTHETIC_MODEL_CATALOG.map(buildSyntheticModelDefinition), + aliases: [{ modelRef: SYNTHETIC_DEFAULT_MODEL_REF, alias: "MiniMax M2.5" }], + primaryModelRef, }); } -export function applySyntheticConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applySyntheticProviderConfig(cfg), - SYNTHETIC_DEFAULT_MODEL_REF, - ); +export function applySyntheticProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applySyntheticPreset(cfg); +} + +export function applySyntheticConfig(cfg: OpenClawConfig): OpenClawConfig { + return applySyntheticPreset(cfg, SYNTHETIC_DEFAULT_MODEL_REF); } diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index b6c3c01763c..6b9e2a766d2 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -6,10 +6,9 @@ import { modelSupportsVision, } from "openclaw/plugin-sdk/agent-runtime"; import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime"; +import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; import { removeAckReactionAfterReply } from "openclaw/plugin-sdk/channel-runtime"; import { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-runtime"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime"; -import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { loadSessionStore, @@ -381,12 +380,6 @@ export const dispatchTelegramMessage = async ({ ? true : undefined; - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ - cfg, - agentId: route.agentId, - channel: "telegram", - accountId: route.accountId, - }); const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId); // Handle uncached stickers: get a dedicated vision description before dispatch @@ -524,15 +517,21 @@ export const dispatchTelegramMessage = async ({ void statusReactionController.setThinking(); } - const typingCallbacks = createTypingCallbacks({ - start: sendTyping, - onStartError: (err) => { - logTypingFailure({ - log: logVerbose, - channel: "telegram", - target: String(chatId), - error: err, - }); + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ + cfg, + agentId: route.agentId, + channel: "telegram", + accountId: route.accountId, + typing: { + start: sendTyping, + onStartError: (err) => { + logTypingFailure({ + log: logVerbose, + channel: "telegram", + target: String(chatId), + error: err, + }); + }, }, }); @@ -542,8 +541,7 @@ export const dispatchTelegramMessage = async ({ ctx: ctxPayload, cfg, dispatcherOptions: { - ...prefixOptions, - typingCallbacks, + ...replyPipeline, deliver: async (payload, info) => { if (payload.isError === true) { hadErrorReplyFailureOrSkip = true; diff --git a/extensions/together/onboard.ts b/extensions/together/onboard.ts index e18595ab21e..f23b5b5dbda 100644 --- a/extensions/together/onboard.ts +++ b/extensions/together/onboard.ts @@ -4,32 +4,27 @@ import { TOGETHER_MODEL_CATALOG, } from "openclaw/plugin-sdk/provider-models"; import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5"; -export function applyTogetherProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[TOGETHER_DEFAULT_MODEL_REF] = { - ...models[TOGETHER_DEFAULT_MODEL_REF], - alias: models[TOGETHER_DEFAULT_MODEL_REF]?.alias ?? "Together AI", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, +function applyTogetherPreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "together", api: "openai-completions", baseUrl: TOGETHER_BASE_URL, catalogModels: TOGETHER_MODEL_CATALOG.map(buildTogetherModelDefinition), + aliases: [{ modelRef: TOGETHER_DEFAULT_MODEL_REF, alias: "Together AI" }], + primaryModelRef, }); } -export function applyTogetherConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyTogetherProviderConfig(cfg), - TOGETHER_DEFAULT_MODEL_REF, - ); +export function applyTogetherProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyTogetherPreset(cfg); +} + +export function applyTogetherConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyTogetherPreset(cfg, TOGETHER_DEFAULT_MODEL_REF); } diff --git a/extensions/venice/onboard.ts b/extensions/venice/onboard.ts index 23634a18540..5d3787bb171 100644 --- a/extensions/venice/onboard.ts +++ b/extensions/venice/onboard.ts @@ -5,29 +5,27 @@ import { VENICE_MODEL_CATALOG, } from "openclaw/plugin-sdk/provider-models"; import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; export { VENICE_DEFAULT_MODEL_REF }; -export function applyVeniceProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[VENICE_DEFAULT_MODEL_REF] = { - ...models[VENICE_DEFAULT_MODEL_REF], - alias: models[VENICE_DEFAULT_MODEL_REF]?.alias ?? "Kimi K2.5", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, +function applyVenicePreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig { + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "venice", api: "openai-completions", baseUrl: VENICE_BASE_URL, catalogModels: VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition), + aliases: [{ modelRef: VENICE_DEFAULT_MODEL_REF, alias: "Kimi K2.5" }], + primaryModelRef, }); } -export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyVeniceProviderConfig(cfg), VENICE_DEFAULT_MODEL_REF); +export function applyVeniceProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyVenicePreset(cfg); +} + +export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyVenicePreset(cfg, VENICE_DEFAULT_MODEL_REF); } diff --git a/extensions/xai/onboard.ts b/extensions/xai/onboard.ts index 75cf2b97d13..d137631d2cf 100644 --- a/extensions/xai/onboard.ts +++ b/extensions/xai/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithDefaultModels, + applyProviderConfigWithDefaultModelsPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { XAI_BASE_URL, XAI_DEFAULT_MODEL_ID } from "./model-definitions.js"; @@ -11,20 +10,16 @@ export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`; function applyXaiProviderConfigWithApi( cfg: OpenClawConfig, api: "openai-completions" | "openai-responses", + primaryModelRef?: string, ): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[XAI_DEFAULT_MODEL_REF] = { - ...models[XAI_DEFAULT_MODEL_REF], - alias: models[XAI_DEFAULT_MODEL_REF]?.alias ?? "Grok", - }; - - return applyProviderConfigWithDefaultModels(cfg, { - agentModels: models, + return applyProviderConfigWithDefaultModelsPreset(cfg, { providerId: "xai", api, baseUrl: XAI_BASE_URL, defaultModels: buildXaiCatalogModels(), defaultModelId: XAI_DEFAULT_MODEL_ID, + aliases: [{ modelRef: XAI_DEFAULT_MODEL_REF, alias: "Grok" }], + primaryModelRef, }); } @@ -37,5 +32,5 @@ export function applyXaiResponsesApiConfig(cfg: OpenClawConfig): OpenClawConfig } export function applyXaiConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary(applyXaiProviderConfig(cfg), XAI_DEFAULT_MODEL_REF); + return applyXaiProviderConfigWithApi(cfg, "openai-completions", XAI_DEFAULT_MODEL_REF); } diff --git a/extensions/zai/onboard.ts b/extensions/zai/onboard.ts index aa756546302..18bf8c3aa45 100644 --- a/extensions/zai/onboard.ts +++ b/extensions/zai/onboard.ts @@ -1,6 +1,5 @@ import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { @@ -19,32 +18,35 @@ const ZAI_DEFAULT_MODELS = [ buildZaiModelDefinition({ id: "glm-4.7-flashx" }), ]; +function resolveZaiPresetBaseUrl(cfg: OpenClawConfig, endpoint?: string): string { + const existingProvider = cfg.models?.providers?.zai; + const existingBaseUrl = + typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; + return endpoint ? resolveZaiBaseUrl(endpoint) : existingBaseUrl || resolveZaiBaseUrl(); +} + +function applyZaiPreset( + cfg: OpenClawConfig, + params?: { endpoint?: string; modelId?: string }, + primaryModelRef?: string, +): OpenClawConfig { + const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID; + const modelRef = `zai/${modelId}`; + return applyProviderConfigWithModelCatalogPreset(cfg, { + providerId: "zai", + api: "openai-completions", + baseUrl: resolveZaiPresetBaseUrl(cfg, params?.endpoint), + catalogModels: ZAI_DEFAULT_MODELS, + aliases: [{ modelRef, alias: "GLM" }], + primaryModelRef, + }); +} + export function applyZaiProviderConfig( cfg: OpenClawConfig, params?: { endpoint?: string; modelId?: string }, ): OpenClawConfig { - const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID; - const modelRef = `zai/${modelId}`; - const existingProvider = cfg.models?.providers?.zai; - const models = { ...cfg.agents?.defaults?.models }; - models[modelRef] = { - ...models[modelRef], - alias: models[modelRef]?.alias ?? "GLM", - }; - - const existingBaseUrl = - typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; - const baseUrl = params?.endpoint - ? resolveZaiBaseUrl(params.endpoint) - : existingBaseUrl || resolveZaiBaseUrl(); - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, - providerId: "zai", - api: "openai-completions", - baseUrl, - catalogModels: ZAI_DEFAULT_MODELS, - }); + return applyZaiPreset(cfg, params); } export function applyZaiConfig( @@ -53,5 +55,5 @@ export function applyZaiConfig( ): OpenClawConfig { const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID; const modelRef = modelId === ZAI_DEFAULT_MODEL_ID ? ZAI_DEFAULT_MODEL_REF : `zai/${modelId}`; - return applyAgentDefaultModelPrimary(applyZaiProviderConfig(cfg, params), modelRef); + return applyZaiPreset(cfg, params, modelRef); } diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index b21476fbf8f..ad36b1f27d5 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -30,11 +30,9 @@ import { import { resolveZaloProxyFetch } from "./proxy.js"; import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "./runtime-api.js"; import { - createTypingCallbacks, - createScopedPairingAccess, - createReplyPrefixOptions, + createChannelPairingController, + createChannelReplyPipeline, deliverTextOrMediaReply, - issuePairingChallenge, resolveWebhookPath, logTypingFailure, resolveDefaultGroupPolicy, @@ -330,7 +328,7 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr statusSink, fetcher, } = params; - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "zalo", accountId: account.accountId, @@ -406,12 +404,10 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr } if (directDmOutcome === "unauthorized") { if (dmPolicy === "pairing") { - await issuePairingChallenge({ - channel: "zalo", + await pairing.issueChallenge({ senderId, senderIdLine: `Your Zalo user id: ${senderId}`, meta: { name: senderName ?? undefined }, - upsertPairingRequest: pairing.upsertPairingRequest, onCreated: () => { logVerbose(core, runtime, `zalo pairing request sender=${senderId}`); }, @@ -507,32 +503,32 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr channel: "zalo", accountId: account.accountId, }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg: config, agentId: route.agentId, channel: "zalo", accountId: account.accountId, - }); - const typingCallbacks = createTypingCallbacks({ - start: async () => { - await sendChatAction( - token, - { - chat_id: chatId, - action: "typing", - }, - fetcher, - ZALO_TYPING_TIMEOUT_MS, - ); - }, - onStartError: (err) => { - logTypingFailure({ - log: (message) => logVerbose(core, runtime, message), - channel: "zalo", - action: "start", - target: chatId, - error: err, - }); + typing: { + start: async () => { + await sendChatAction( + token, + { + chat_id: chatId, + action: "typing", + }, + fetcher, + ZALO_TYPING_TIMEOUT_MS, + ); + }, + onStartError: (err) => { + logTypingFailure({ + log: (message) => logVerbose(core, runtime, message), + channel: "zalo", + action: "start", + target: chatId, + error: err, + }); + }, }, }); @@ -540,8 +536,7 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr ctx: ctxPayload, cfg: config, dispatcherOptions: { - ...prefixOptions, - typingCallbacks, + ...replyPipeline, deliver: async (payload) => { await deliverZaloReply({ payload, diff --git a/extensions/zalo/src/secret-input.ts b/extensions/zalo/src/secret-input.ts index b32083456e7..f1b2aae5c92 100644 --- a/extensions/zalo/src/secret-input.ts +++ b/extensions/zalo/src/secret-input.ts @@ -1,13 +1,6 @@ -import { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "./runtime-api.js"; - export { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -}; +} from "openclaw/plugin-sdk/secret-input"; diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 7f455d93166..1a807a1a1b9 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -18,13 +18,11 @@ import type { RuntimeEnv, } from "../runtime-api.js"; import { - createTypingCallbacks, - createScopedPairingAccess, - createReplyPrefixOptions, + createChannelPairingController, + createChannelReplyPipeline, deliverTextOrMediaReply, evaluateGroupRouteAccessForPolicy, isDangerousNameMatchingEnabled, - issuePairingChallenge, mergeAllowlist, resolveMentionGatingWithBypass, resolveOpenProviderRuntimeGroupPolicy, @@ -252,7 +250,7 @@ async function processMessage( historyState: ZalouserGroupHistoryState, statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, ): Promise { - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "zalouser", accountId: account.accountId, @@ -389,12 +387,10 @@ async function processMessage( if (!isGroup && accessDecision.decision !== "allow") { if (accessDecision.decision === "pairing") { - await issuePairingChallenge({ - channel: "zalouser", + await pairing.issueChallenge({ senderId, senderIdLine: `Your Zalo user id: ${senderId}`, meta: { name: senderName || undefined }, - upsertPairingRequest: pairing.upsertPairingRequest, onCreated: () => { logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`); }, @@ -630,24 +626,24 @@ async function processMessage( }, }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg: config, agentId: route.agentId, channel: "zalouser", accountId: account.accountId, - }); - const typingCallbacks = createTypingCallbacks({ - start: async () => { - await sendTypingZalouser(chatId, { - profile: account.profile, - isGroup, - }); - }, - onStartError: (err) => { - runtime.error?.( - `[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`, - ); - logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`); + typing: { + start: async () => { + await sendTypingZalouser(chatId, { + profile: account.profile, + isGroup, + }); + }, + onStartError: (err) => { + runtime.error?.( + `[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`, + ); + logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`); + }, }, }); @@ -655,8 +651,7 @@ async function processMessage( ctx: ctxPayload, cfg: config, dispatcherOptions: { - ...prefixOptions, - typingCallbacks, + ...replyPipeline, deliver: async (payload) => { await deliverZalouserReply({ payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string }, diff --git a/package.json b/package.json index be13ed078ea..7b503e34ab9 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,10 @@ "types": "./dist/plugin-sdk/setup.d.ts", "default": "./dist/plugin-sdk/setup.js" }, + "./plugin-sdk/channel-setup": { + "types": "./dist/plugin-sdk/channel-setup.d.ts", + "default": "./dist/plugin-sdk/channel-setup.js" + }, "./plugin-sdk/setup-tools": { "types": "./dist/plugin-sdk/setup-tools.d.ts", "default": "./dist/plugin-sdk/setup-tools.js" @@ -94,6 +98,10 @@ "types": "./dist/plugin-sdk/reply-payload.d.ts", "default": "./dist/plugin-sdk/reply-payload.js" }, + "./plugin-sdk/channel-reply-pipeline": { + "types": "./dist/plugin-sdk/channel-reply-pipeline.d.ts", + "default": "./dist/plugin-sdk/channel-reply-pipeline.js" + }, "./plugin-sdk/channel-runtime": { "types": "./dist/plugin-sdk/channel-runtime.d.ts", "default": "./dist/plugin-sdk/channel-runtime.js" @@ -254,6 +262,10 @@ "types": "./dist/plugin-sdk/channel-lifecycle.d.ts", "default": "./dist/plugin-sdk/channel-lifecycle.js" }, + "./plugin-sdk/channel-pairing": { + "types": "./dist/plugin-sdk/channel-pairing.d.ts", + "default": "./dist/plugin-sdk/channel-pairing.js" + }, "./plugin-sdk/channel-policy": { "types": "./dist/plugin-sdk/channel-policy.d.ts", "default": "./dist/plugin-sdk/channel-policy.js" @@ -334,6 +346,10 @@ "types": "./dist/plugin-sdk/request-url.d.ts", "default": "./dist/plugin-sdk/request-url.js" }, + "./plugin-sdk/webhook-ingress": { + "types": "./dist/plugin-sdk/webhook-ingress.d.ts", + "default": "./dist/plugin-sdk/webhook-ingress.js" + }, "./plugin-sdk/webhook-path": { "types": "./dist/plugin-sdk/webhook-path.d.ts", "default": "./dist/plugin-sdk/webhook-path.js" @@ -342,6 +358,10 @@ "types": "./dist/plugin-sdk/runtime-store.d.ts", "default": "./dist/plugin-sdk/runtime-store.js" }, + "./plugin-sdk/secret-input": { + "types": "./dist/plugin-sdk/secret-input.d.ts", + "default": "./dist/plugin-sdk/secret-input.js" + }, "./plugin-sdk/web-media": { "types": "./dist/plugin-sdk/web-media.d.ts", "default": "./dist/plugin-sdk/web-media.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 04919191231..282052b23f5 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -9,10 +9,12 @@ "runtime", "runtime-env", "setup", + "channel-setup", "setup-tools", "config-runtime", "reply-runtime", "reply-payload", + "channel-reply-pipeline", "channel-runtime", "interactive-runtime", "infra-runtime", @@ -53,6 +55,7 @@ "channel-config-helpers", "channel-config-schema", "channel-lifecycle", + "channel-pairing", "channel-policy", "channel-send-result", "group-access", @@ -73,8 +76,10 @@ "reply-history", "media-understanding", "request-url", + "webhook-ingress", "webhook-path", "runtime-store", + "secret-input", "web-media", "speech", "state-paths", diff --git a/src/commands/onboard-auth.config-shared.test.ts b/src/commands/onboard-auth.config-shared.test.ts index 01cda96ae74..ecdfd227094 100644 --- a/src/commands/onboard-auth.config-shared.test.ts +++ b/src/commands/onboard-auth.config-shared.test.ts @@ -3,9 +3,12 @@ import type { OpenClawConfig } from "../config/config.js"; import type { AgentModelEntryConfig } from "../config/types.agent-defaults.js"; import type { ModelDefinitionConfig } from "../config/types.models.js"; import { + applyProviderConfigWithDefaultModelPreset, + applyProviderConfigWithModelCatalogPreset, applyProviderConfigWithDefaultModel, applyProviderConfigWithDefaultModels, applyProviderConfigWithModelCatalog, + withAgentModelAliases, } from "../plugins/provider-onboarding-config.js"; function makeModel(id: string): ModelDefinitionConfig { @@ -97,4 +100,76 @@ describe("onboard auth provider config merges", () => { expect(next.models?.providers?.custom?.models?.map((m) => m.id)).toEqual(["model-z"]); }); + + it("preserves explicit aliases when adding provider alias presets", () => { + expect( + withAgentModelAliases( + { + "custom/model-a": { alias: "Pinned" }, + }, + [{ modelRef: "custom/model-a", alias: "Preset" }, "custom/model-b"], + ), + ).toEqual({ + "custom/model-a": { alias: "Pinned" }, + "custom/model-b": {}, + }); + }); + + it("applies default-model presets with alias and primary model", () => { + const next = applyProviderConfigWithDefaultModelPreset( + { + agents: { + defaults: { + models: { + "custom/model-z": { alias: "Pinned" }, + }, + }, + }, + }, + { + providerId: "custom", + api: "openai-completions", + baseUrl: "https://example.com/v1", + defaultModel: makeModel("model-z"), + aliases: [{ modelRef: "custom/model-z", alias: "Preset" }], + primaryModelRef: "custom/model-z", + }, + ); + + expect(next.agents?.defaults?.models?.["custom/model-z"]).toEqual({ alias: "Pinned" }); + expect(next.agents?.defaults?.model).toEqual({ primary: "custom/model-z" }); + }); + + it("applies catalog presets with alias and merged catalog models", () => { + const next = applyProviderConfigWithModelCatalogPreset( + { + models: { + providers: { + custom: { + api: "openai-completions", + baseUrl: "https://example.com/v1", + models: [makeModel("model-a")], + }, + }, + }, + }, + { + providerId: "custom", + api: "openai-completions", + baseUrl: "https://example.com/v1", + catalogModels: [makeModel("model-a"), makeModel("model-b")], + aliases: [{ modelRef: "custom/model-b", alias: "Catalog Alias" }], + primaryModelRef: "custom/model-b", + }, + ); + + expect(next.models?.providers?.custom?.models?.map((model) => model.id)).toEqual([ + "model-a", + "model-b", + ]); + expect(next.agents?.defaults?.models?.["custom/model-b"]).toEqual({ + alias: "Catalog Alias", + }); + expect(next.agents?.defaults?.model).toEqual({ primary: "custom/model-b" }); + }); }); diff --git a/src/plugin-sdk/channel-pairing.test.ts b/src/plugin-sdk/channel-pairing.test.ts new file mode 100644 index 00000000000..7caac389c9b --- /dev/null +++ b/src/plugin-sdk/channel-pairing.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vitest"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import { createChannelPairingController } from "./channel-pairing.js"; + +describe("createChannelPairingController", () => { + it("scopes store access and issues pairing challenges through the scoped store", async () => { + const readAllowFromStore = vi.fn(async () => ["alice"]); + const upsertPairingRequest = vi.fn(async () => ({ code: "123456", created: true })); + const replies: string[] = []; + const sendPairingReply = vi.fn(async (text: string) => { + replies.push(text); + }); + const runtime = { + channel: { + pairing: { + readAllowFromStore, + upsertPairingRequest, + }, + }, + } as unknown as PluginRuntime; + + const pairing = createChannelPairingController({ + core: runtime, + channel: "googlechat", + accountId: "Primary", + }); + + await expect(pairing.readAllowFromStore()).resolves.toEqual(["alice"]); + await pairing.issueChallenge({ + senderId: "user-1", + senderIdLine: "Your id: user-1", + sendPairingReply, + }); + + expect(readAllowFromStore).toHaveBeenCalledWith({ + channel: "googlechat", + accountId: "primary", + }); + expect(upsertPairingRequest).toHaveBeenCalledWith({ + channel: "googlechat", + accountId: "primary", + id: "user-1", + meta: undefined, + }); + expect(sendPairingReply).toHaveBeenCalledTimes(1); + expect(replies[0]).toContain("123456"); + }); +}); diff --git a/src/plugin-sdk/channel-pairing.ts b/src/plugin-sdk/channel-pairing.ts new file mode 100644 index 00000000000..2628eebfde8 --- /dev/null +++ b/src/plugin-sdk/channel-pairing.ts @@ -0,0 +1,31 @@ +import type { ChannelId } from "../channels/plugins/types.js"; +import { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import { createScopedPairingAccess } from "./pairing-access.js"; + +export { createScopedPairingAccess } from "./pairing-access.js"; + +type ScopedPairingAccess = ReturnType; + +export type ChannelPairingController = ScopedPairingAccess & { + issueChallenge: ( + params: Omit[0], "channel" | "upsertPairingRequest">, + ) => ReturnType; +}; + +export function createChannelPairingController(params: { + core: PluginRuntime; + channel: ChannelId; + accountId: string; +}): ChannelPairingController { + const access = createScopedPairingAccess(params); + return { + ...access, + issueChallenge: (challenge) => + issuePairingChallenge({ + channel: params.channel, + upsertPairingRequest: access.upsertPairingRequest, + ...challenge, + }), + }; +} diff --git a/src/plugin-sdk/channel-reply-pipeline.test.ts b/src/plugin-sdk/channel-reply-pipeline.test.ts new file mode 100644 index 00000000000..cc8c15e4b16 --- /dev/null +++ b/src/plugin-sdk/channel-reply-pipeline.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from "vitest"; +import { createChannelReplyPipeline } from "./channel-reply-pipeline.js"; + +describe("createChannelReplyPipeline", () => { + it("builds prefix options without forcing typing support", () => { + const pipeline = createChannelReplyPipeline({ + cfg: {}, + agentId: "main", + channel: "telegram", + accountId: "default", + }); + + expect(typeof pipeline.onModelSelected).toBe("function"); + expect(typeof pipeline.responsePrefixContextProvider).toBe("function"); + expect(pipeline.typingCallbacks).toBeUndefined(); + }); + + it("builds typing callbacks when typing config is provided", async () => { + const start = vi.fn(async () => {}); + const stop = vi.fn(async () => {}); + const pipeline = createChannelReplyPipeline({ + cfg: {}, + agentId: "main", + channel: "discord", + accountId: "default", + typing: { + start, + stop, + onStartError: () => {}, + }, + }); + + await pipeline.typingCallbacks?.onReplyStart(); + pipeline.typingCallbacks?.onIdle?.(); + + expect(start).toHaveBeenCalled(); + expect(stop).toHaveBeenCalled(); + }); +}); diff --git a/src/plugin-sdk/channel-reply-pipeline.ts b/src/plugin-sdk/channel-reply-pipeline.ts new file mode 100644 index 00000000000..a2244ade7f1 --- /dev/null +++ b/src/plugin-sdk/channel-reply-pipeline.ts @@ -0,0 +1,38 @@ +import { + createReplyPrefixContext, + createReplyPrefixOptions, + type ReplyPrefixContextBundle, + type ReplyPrefixOptions, +} from "../channels/reply-prefix.js"; +import { + createTypingCallbacks, + type CreateTypingCallbacksParams, + type TypingCallbacks, +} from "../channels/typing.js"; + +export type ReplyPrefixContext = ReplyPrefixContextBundle["prefixContext"]; +export type { ReplyPrefixContextBundle, ReplyPrefixOptions }; +export type { CreateTypingCallbacksParams, TypingCallbacks }; +export { createReplyPrefixContext, createReplyPrefixOptions, createTypingCallbacks }; + +export type ChannelReplyPipeline = ReplyPrefixOptions & { + typingCallbacks?: TypingCallbacks; +}; + +export function createChannelReplyPipeline(params: { + cfg: Parameters[0]["cfg"]; + agentId: string; + channel?: string; + accountId?: string; + typing?: CreateTypingCallbacksParams; +}): ChannelReplyPipeline { + return { + ...createReplyPrefixOptions({ + cfg: params.cfg, + agentId: params.agentId, + channel: params.channel, + accountId: params.accountId, + }), + ...(params.typing ? { typingCallbacks: createTypingCallbacks(params.typing) } : {}), + }; +} diff --git a/src/plugin-sdk/channel-setup.test.ts b/src/plugin-sdk/channel-setup.test.ts new file mode 100644 index 00000000000..3890dfc803d --- /dev/null +++ b/src/plugin-sdk/channel-setup.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; + +describe("createOptionalChannelSetupSurface", () => { + it("returns a matched adapter and wizard for optional plugins", async () => { + const setup = createOptionalChannelSetupSurface({ + channel: "example", + label: "Example", + npmSpec: "@openclaw/example", + docsPath: "/channels/example", + }); + + expect(setup.setupAdapter.resolveAccountId?.({ cfg: {} })).toBe("default"); + expect( + setup.setupAdapter.validateInput?.({ + cfg: {}, + accountId: "default", + input: {}, + }), + ).toContain("@openclaw/example"); + expect(setup.setupWizard.channel).toBe("example"); + expect(setup.setupWizard.status.unconfiguredHint).toContain("/channels/example"); + await expect( + setup.setupWizard.finalize?.({ + cfg: {}, + accountId: "default", + credentialValues: {}, + runtime: { + log: () => {}, + error: () => {}, + exit: async () => {}, + }, + prompter: {} as never, + forceAllowFrom: false, + }), + ).rejects.toThrow("@openclaw/example"); + }); +}); diff --git a/src/plugin-sdk/channel-setup.ts b/src/plugin-sdk/channel-setup.ts new file mode 100644 index 00000000000..6488bd1a770 --- /dev/null +++ b/src/plugin-sdk/channel-setup.ts @@ -0,0 +1,42 @@ +import type { ChannelSetupWizard } from "../channels/plugins/setup-wizard.js"; +import type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + +export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; +export type { ChannelSetupDmPolicy, ChannelSetupWizard } from "./setup.js"; +export { + DEFAULT_ACCOUNT_ID, + createTopLevelChannelDmPolicy, + formatDocsLink, + setSetupChannelEnabled, + splitSetupEntries, +} from "./setup.js"; + +type OptionalChannelSetupParams = { + channel: string; + label: string; + npmSpec?: string; + docsPath?: string; +}; + +export type OptionalChannelSetupSurface = { + setupAdapter: ChannelSetupAdapter; + setupWizard: ChannelSetupWizard; +}; + +export { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + +export function createOptionalChannelSetupSurface( + params: OptionalChannelSetupParams, +): OptionalChannelSetupSurface { + return { + setupAdapter: createOptionalChannelSetupAdapter(params), + setupWizard: createOptionalChannelSetupWizard(params), + }; +} diff --git a/src/plugin-sdk/feishu.ts b/src/plugin-sdk/feishu.ts index cde08767535..f0ecb31650b 100644 --- a/src/plugin-sdk/feishu.ts +++ b/src/plugin-sdk/feishu.ts @@ -38,7 +38,7 @@ export type { } from "../channels/plugins/types.adapters.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; export { createReplyPrefixContext } from "../channels/reply-prefix.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline, createTypingCallbacks } from "./channel-reply-pipeline.js"; export type { OpenClawConfig as ClawdbotConfig, OpenClawConfig } from "../config/config.js"; export { resolveAllowlistProviderRuntimeGroupPolicy, @@ -47,13 +47,13 @@ export { warnMissingProviderGroupPolicyFallbackOnce, } from "../config/runtime-group-policy.js"; export type { DmPolicy, GroupToolPolicyConfig } from "../config/types.js"; -export type { SecretInput } from "../config/types.secrets.js"; +export type { SecretInput } from "./secret-input.js"; export { + buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; +} from "./secret-input.js"; export { createDedupeCache } from "../infra/dedupe.js"; export { installRequestBodyLimitGuard, readJsonBodyWithLimit } from "../infra/http-body.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; @@ -70,8 +70,7 @@ export type { WizardPrompter } from "../wizard/prompts.js"; export { feishuSetupWizard, feishuSetupAdapter } from "../../extensions/feishu/setup-api.js"; export { buildAgentMediaPayload } from "./agent-media-payload.js"; export { readJsonFileWithFallback } from "./json-store.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { createPersistentDedupe } from "./persistent-dedupe.js"; export { buildBaseChannelStatusSummary, @@ -85,9 +84,9 @@ export { parseFeishuConversationId, } from "../../extensions/feishu/src/conversation-id.js"; export { - createFixedWindowRateLimiter, createWebhookAnomalyTracker, + createFixedWindowRateLimiter, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, -} from "./webhook-memory-guards.js"; -export { applyBasicWebhookRequestGuards } from "./webhook-request-guards.js"; +} from "./webhook-ingress.js"; +export { applyBasicWebhookRequestGuards } from "./webhook-ingress.js"; diff --git a/src/plugin-sdk/googlechat.ts b/src/plugin-sdk/googlechat.ts index bbb818b78b8..a12b4fe6e47 100644 --- a/src/plugin-sdk/googlechat.ts +++ b/src/plugin-sdk/googlechat.ts @@ -2,10 +2,7 @@ // Keep this list additive and scoped to symbols used under extensions/googlechat. import { resolveChannelGroupRequireMention } from "./channel-policy.js"; -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export { createActionGate, @@ -49,7 +46,7 @@ export type { } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; export { getChatChannelMeta } from "../channels/registry.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { @@ -71,26 +68,23 @@ export { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy, } from "./group-access.js"; export { extractToolSend } from "./tool-send.js"; -export { resolveWebhookPath } from "./webhook-path.js"; -export type { WebhookInFlightLimiter } from "./webhook-request-guards.js"; export { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, readJsonWebhookBodyOrReject, -} from "./webhook-request-guards.js"; -export { registerWebhookTargetWithPluginRoute, - resolveWebhookTargets, + resolveWebhookPath, resolveWebhookTargetWithAuthOrReject, + resolveWebhookTargets, + type WebhookInFlightLimiter, withResolvedWebhookRequestPipeline, -} from "./webhook-targets.js"; +} from "./webhook-ingress.js"; type GoogleChatGroupContext = { cfg: import("../config/config.js").OpenClawConfig; @@ -107,16 +101,12 @@ export function resolveGoogleChatGroupRequireMention(params: GoogleChatGroupCont }); } -export const googlechatSetupAdapter = createOptionalChannelSetupAdapter({ +const googlechatSetup = createOptionalChannelSetupSurface({ channel: "googlechat", label: "Google Chat", npmSpec: "@openclaw/googlechat", docsPath: "/channels/googlechat", }); -export const googlechatSetupWizard = createOptionalChannelSetupWizard({ - channel: "googlechat", - label: "Google Chat", - npmSpec: "@openclaw/googlechat", - docsPath: "/channels/googlechat", -}); +export const googlechatSetupAdapter = googlechatSetup.setupAdapter; +export const googlechatSetupWizard = googlechatSetup.setupWizard; diff --git a/src/plugin-sdk/irc.ts b/src/plugin-sdk/irc.ts index b64614348cb..66fe825f45b 100644 --- a/src/plugin-sdk/irc.ts +++ b/src/plugin-sdk/irc.ts @@ -23,7 +23,7 @@ export { patchScopedAccountConfig } from "../channels/plugins/setup-helpers.js"; export type { BaseProbeResult } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; export { getChatChannelMeta } from "../channels/registry.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { @@ -69,8 +69,7 @@ export { } from "../security/dm-policy-shared.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { dispatchInboundReplyWithBase } from "./inbound-reply-dispatch.js"; export { ircSetupAdapter, ircSetupWizard } from "../../extensions/irc/api.js"; export type { OutboundReplyPayload } from "./reply-payload.js"; diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts index 5bbaac2ce48..92785e4d97b 100644 --- a/src/plugin-sdk/matrix.ts +++ b/src/plugin-sdk/matrix.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled matrix plugin. // Keep this list additive and scoped to symbols used under extensions/matrix. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export { createActionGate, @@ -60,8 +57,8 @@ export type { ChannelToolSend, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; +export { createTypingCallbacks } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { GROUP_POLICY_BLOCKED_LABEL, @@ -75,13 +72,13 @@ export type { GroupToolPolicyConfig, MarkdownTableMode, } from "../config/types.js"; -export type { SecretInput } from "../config/types.secrets.js"; +export type { SecretInput } from "./secret-input.js"; export { + buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; +} from "./secret-input.js"; export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; @@ -103,7 +100,7 @@ export { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy, } from "./group-access.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; export { runPluginCommandWithTimeout } from "./run-command.js"; export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; @@ -114,16 +111,12 @@ export { collectStatusIssuesFromLastError, } from "./status-helpers.js"; -export const matrixSetupWizard = createOptionalChannelSetupWizard({ +const matrixSetup = createOptionalChannelSetupSurface({ channel: "matrix", label: "Matrix", npmSpec: "@openclaw/matrix", docsPath: "/channels/matrix", }); -export const matrixSetupAdapter = createOptionalChannelSetupAdapter({ - channel: "matrix", - label: "Matrix", - npmSpec: "@openclaw/matrix", - docsPath: "/channels/matrix", -}); +export const matrixSetupWizard = matrixSetup.setupWizard; +export const matrixSetupAdapter = matrixSetup.setupAdapter; diff --git a/src/plugin-sdk/msteams.ts b/src/plugin-sdk/msteams.ts index 51f8ef257b2..a48843137a0 100644 --- a/src/plugin-sdk/msteams.ts +++ b/src/plugin-sdk/msteams.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled msteams plugin. // Keep this list additive and scoped to symbols used under extensions/msteams. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export type { ChunkMode } from "../auto-reply/chunk.js"; export type { HistoryEntry } from "../auto-reply/reply/history.js"; @@ -55,8 +52,8 @@ export type { ChannelOutboundAdapter, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; +export { createTypingCallbacks } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { resolveToolsBySender } from "../config/group-policy.js"; @@ -109,7 +106,7 @@ export { withFileLock } from "./file-lock.js"; export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js"; export { loadOutboundMediaFromUrl } from "./outbound-media.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { resolveInboundSessionEnvelopeContext } from "../channels/session-envelope.js"; export { buildHostnameAllowlistPolicyFromSuffixAllowlist, @@ -124,16 +121,12 @@ export { } from "./status-helpers.js"; export { normalizeStringEntries } from "../shared/string-normalization.js"; -export const msteamsSetupWizard = createOptionalChannelSetupWizard({ +const msteamsSetup = createOptionalChannelSetupSurface({ channel: "msteams", label: "Microsoft Teams", npmSpec: "@openclaw/msteams", docsPath: "/channels/msteams", }); -export const msteamsSetupAdapter = createOptionalChannelSetupAdapter({ - channel: "msteams", - label: "Microsoft Teams", - npmSpec: "@openclaw/msteams", - docsPath: "/channels/msteams", -}); +export const msteamsSetupWizard = msteamsSetup.setupWizard; +export const msteamsSetupAdapter = msteamsSetup.setupAdapter; diff --git a/src/plugin-sdk/nextcloud-talk.ts b/src/plugin-sdk/nextcloud-talk.ts index e3be0cd868d..b2ab105b844 100644 --- a/src/plugin-sdk/nextcloud-talk.ts +++ b/src/plugin-sdk/nextcloud-talk.ts @@ -32,7 +32,7 @@ export { export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; export type { ChannelGroupContext, ChannelSetupInput } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { mapAllowFromEntries } from "./channel-config-helpers.js"; export { evaluateMatchedGroupAccessForPolicy } from "./group-access.js"; @@ -49,13 +49,13 @@ export type { GroupPolicy, GroupToolPolicyConfig, } from "../config/types.js"; -export type { SecretInput } from "../config/types.secrets.js"; +export type { SecretInput } from "./secret-input.js"; export { + buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; +} from "./secret-input.js"; export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; export { BlockStreamingCoalesceSchema, @@ -88,8 +88,7 @@ export { listConfiguredAccountIds, resolveAccountWithDefaultFallback, } from "./account-resolution.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { createPersistentDedupe } from "./persistent-dedupe.js"; export type { OutboundReplyPayload } from "./reply-payload.js"; export { diff --git a/src/plugin-sdk/nostr.ts b/src/plugin-sdk/nostr.ts index a3bd64e34fc..640642dcd46 100644 --- a/src/plugin-sdk/nostr.ts +++ b/src/plugin-sdk/nostr.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled nostr plugin. // Keep this list additive and scoped to symbols used under extensions/nostr. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; @@ -25,16 +22,12 @@ export { export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js"; export { mapAllowFromEntries } from "./channel-config-helpers.js"; -export const nostrSetupAdapter = createOptionalChannelSetupAdapter({ +const nostrSetup = createOptionalChannelSetupSurface({ channel: "nostr", label: "Nostr", npmSpec: "@openclaw/nostr", docsPath: "/channels/nostr", }); -export const nostrSetupWizard = createOptionalChannelSetupWizard({ - channel: "nostr", - label: "Nostr", - npmSpec: "@openclaw/nostr", - docsPath: "/channels/nostr", -}); +export const nostrSetupAdapter = nostrSetup.setupAdapter; +export const nostrSetupWizard = nostrSetup.setupWizard; diff --git a/src/plugin-sdk/provider-onboard.ts b/src/plugin-sdk/provider-onboard.ts index 35b9287bcc8..1537742f453 100644 --- a/src/plugin-sdk/provider-onboard.ts +++ b/src/plugin-sdk/provider-onboard.ts @@ -9,8 +9,13 @@ export type { export { applyAgentDefaultModelPrimary, applyOnboardAuthAgentModelsAndProviders, + applyProviderConfigWithDefaultModelPreset, + applyProviderConfigWithDefaultModelsPreset, applyProviderConfigWithDefaultModel, applyProviderConfigWithDefaultModels, + applyProviderConfigWithModelCatalogPreset, applyProviderConfigWithModelCatalog, + withAgentModelAliases, } from "../plugins/provider-onboarding-config.js"; +export type { AgentModelAliasEntry } from "../plugins/provider-onboarding-config.js"; export { ensureModelAllowlistEntry } from "../plugins/provider-model-allowlist.js"; diff --git a/src/plugin-sdk/secret-input.test.ts b/src/plugin-sdk/secret-input.test.ts new file mode 100644 index 00000000000..d27cdcf870b --- /dev/null +++ b/src/plugin-sdk/secret-input.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { + buildOptionalSecretInputSchema, + buildSecretInputArraySchema, + normalizeSecretInputString, +} from "./secret-input.js"; + +describe("plugin-sdk secret input helpers", () => { + it("accepts undefined for optional secret input", () => { + expect(buildOptionalSecretInputSchema().safeParse(undefined).success).toBe(true); + }); + + it("accepts arrays of secret inputs", () => { + const result = buildSecretInputArraySchema().safeParse([ + "sk-plain", + { source: "env", provider: "default", id: "OPENAI_API_KEY" }, + ]); + expect(result.success).toBe(true); + }); + + it("normalizes plaintext secret strings", () => { + expect(normalizeSecretInputString(" sk-test ")).toBe("sk-test"); + }); +}); diff --git a/src/plugin-sdk/secret-input.ts b/src/plugin-sdk/secret-input.ts new file mode 100644 index 00000000000..3d1d9175a0a --- /dev/null +++ b/src/plugin-sdk/secret-input.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +import { buildSecretInputSchema } from "./secret-input-schema.js"; + +export type { SecretInput } from "../config/types.secrets.js"; +export { + buildSecretInputSchema, + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +}; + +export function buildOptionalSecretInputSchema() { + return buildSecretInputSchema().optional(); +} + +export function buildSecretInputArraySchema() { + return z.array(buildSecretInputSchema()); +} diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index b4a20dabee9..a7417a1b6d5 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -1,6 +1,9 @@ import * as bluebubblesSdk from "openclaw/plugin-sdk/bluebubbles"; +import * as channelPairingSdk from "openclaw/plugin-sdk/channel-pairing"; +import * as channelReplyPipelineSdk from "openclaw/plugin-sdk/channel-reply-pipeline"; import * as channelRuntimeSdk from "openclaw/plugin-sdk/channel-runtime"; import * as channelSendResultSdk from "openclaw/plugin-sdk/channel-send-result"; +import * as channelSetupSdk from "openclaw/plugin-sdk/channel-setup"; import * as coreSdk from "openclaw/plugin-sdk/core"; import type { ChannelMessageActionContext as CoreChannelMessageActionContext, @@ -18,11 +21,13 @@ import * as replyPayloadSdk from "openclaw/plugin-sdk/reply-payload"; import * as routingSdk from "openclaw/plugin-sdk/routing"; import * as runtimeSdk from "openclaw/plugin-sdk/runtime"; import * as sandboxSdk from "openclaw/plugin-sdk/sandbox"; +import * as secretInputSdk from "openclaw/plugin-sdk/secret-input"; import * as selfHostedProviderSetupSdk from "openclaw/plugin-sdk/self-hosted-provider-setup"; import * as setupSdk from "openclaw/plugin-sdk/setup"; import * as slackSdk from "openclaw/plugin-sdk/slack"; import * as telegramSdk from "openclaw/plugin-sdk/telegram"; import * as testingSdk from "openclaw/plugin-sdk/testing"; +import * as webhookIngressSdk from "openclaw/plugin-sdk/webhook-ingress"; import * as whatsappSdk from "openclaw/plugin-sdk/whatsapp"; import * as whatsappActionRuntimeSdk from "openclaw/plugin-sdk/whatsapp-action-runtime"; import * as whatsappLoginQrSdk from "openclaw/plugin-sdk/whatsapp-login-qr"; @@ -111,6 +116,21 @@ describe("plugin-sdk subpath exports", () => { expect(typeof channelRuntimeSdk.sendPayloadMediaSequenceOrFallback).toBe("function"); }); + it("exports channel setup helpers from the dedicated subpath", () => { + expect(typeof channelSetupSdk.createOptionalChannelSetupSurface).toBe("function"); + expect(typeof channelSetupSdk.createTopLevelChannelDmPolicy).toBe("function"); + }); + + it("exports channel pairing helpers from the dedicated subpath", () => { + expect(typeof channelPairingSdk.createChannelPairingController).toBe("function"); + expect(typeof channelPairingSdk.createScopedPairingAccess).toBe("function"); + }); + + it("exports channel reply pipeline helpers from the dedicated subpath", () => { + expect(typeof channelReplyPipelineSdk.createChannelReplyPipeline).toBe("function"); + expect(typeof channelReplyPipelineSdk.createTypingCallbacks).toBe("function"); + }); + it("exports channel send-result helpers from the dedicated subpath", () => { expect(typeof channelSendResultSdk.attachChannelToResult).toBe("function"); expect(typeof channelSendResultSdk.buildChannelSendResult).toBe("function"); @@ -162,6 +182,18 @@ describe("plugin-sdk subpath exports", () => { expect(typeof sandboxSdk.runPluginCommandWithTimeout).toBe("function"); }); + it("exports secret input helpers from the dedicated subpath", () => { + expect(typeof secretInputSdk.buildSecretInputSchema).toBe("function"); + expect(typeof secretInputSdk.buildOptionalSecretInputSchema).toBe("function"); + expect(typeof secretInputSdk.normalizeSecretInputString).toBe("function"); + }); + + it("exports webhook ingress helpers from the dedicated subpath", () => { + expect(typeof webhookIngressSdk.resolveWebhookPath).toBe("function"); + expect(typeof webhookIngressSdk.readJsonWebhookBodyOrReject).toBe("function"); + expect(typeof webhookIngressSdk.withResolvedWebhookRequestPipeline).toBe("function"); + }); + it("exports shared core types used by bundled channels", () => { expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); diff --git a/src/plugin-sdk/tlon.ts b/src/plugin-sdk/tlon.ts index cd11ca66545..6491723ede0 100644 --- a/src/plugin-sdk/tlon.ts +++ b/src/plugin-sdk/tlon.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled tlon plugin. // Keep this list additive and scoped to symbols used under extensions/tlon. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export type { ReplyPayload } from "../auto-reply/types.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; @@ -18,7 +15,7 @@ export type { ChannelSetupInput, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { createDedupeCache } from "../infra/dedupe.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; @@ -33,16 +30,12 @@ export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export { createLoggerBackedRuntime } from "./runtime.js"; -export const tlonSetupAdapter = createOptionalChannelSetupAdapter({ +const tlonSetup = createOptionalChannelSetupSurface({ channel: "tlon", label: "Tlon", npmSpec: "@openclaw/tlon", docsPath: "/channels/tlon", }); -export const tlonSetupWizard = createOptionalChannelSetupWizard({ - channel: "tlon", - label: "Tlon", - npmSpec: "@openclaw/tlon", - docsPath: "/channels/tlon", -}); +export const tlonSetupAdapter = tlonSetup.setupAdapter; +export const tlonSetupWizard = tlonSetup.setupWizard; diff --git a/src/plugin-sdk/twitch.ts b/src/plugin-sdk/twitch.ts index 77bba58209e..b520c6dfdac 100644 --- a/src/plugin-sdk/twitch.ts +++ b/src/plugin-sdk/twitch.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled twitch plugin. // Keep this list additive and scoped to symbols used under extensions/twitch. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export type { ReplyPayload } from "../auto-reply/types.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; @@ -27,7 +24,7 @@ export type { ChannelStatusIssue, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; export type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; @@ -39,14 +36,11 @@ export type { RuntimeEnv } from "../runtime.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; -export const twitchSetupAdapter = createOptionalChannelSetupAdapter({ +const twitchSetup = createOptionalChannelSetupSurface({ channel: "twitch", label: "Twitch", npmSpec: "@openclaw/twitch", }); -export const twitchSetupWizard = createOptionalChannelSetupWizard({ - channel: "twitch", - label: "Twitch", - npmSpec: "@openclaw/twitch", -}); +export const twitchSetupAdapter = twitchSetup.setupAdapter; +export const twitchSetupWizard = twitchSetup.setupWizard; diff --git a/src/plugin-sdk/webhook-ingress.ts b/src/plugin-sdk/webhook-ingress.ts new file mode 100644 index 00000000000..c76e986c050 --- /dev/null +++ b/src/plugin-sdk/webhook-ingress.ts @@ -0,0 +1,38 @@ +export { + createBoundedCounter, + createFixedWindowRateLimiter, + createWebhookAnomalyTracker, + WEBHOOK_ANOMALY_COUNTER_DEFAULTS, + WEBHOOK_ANOMALY_STATUS_CODES, + WEBHOOK_RATE_LIMIT_DEFAULTS, + type BoundedCounter, + type FixedWindowRateLimiter, + type WebhookAnomalyTracker, +} from "./webhook-memory-guards.js"; +export { + applyBasicWebhookRequestGuards, + beginWebhookRequestPipelineOrReject, + createWebhookInFlightLimiter, + isJsonContentType, + readJsonWebhookBodyOrReject, + readWebhookBodyOrReject, + WEBHOOK_BODY_READ_DEFAULTS, + WEBHOOK_IN_FLIGHT_DEFAULTS, + type WebhookBodyReadProfile, + type WebhookInFlightLimiter, +} from "./webhook-request-guards.js"; +export { + registerWebhookTarget, + registerWebhookTargetWithPluginRoute, + resolveSingleWebhookTarget, + resolveSingleWebhookTargetAsync, + resolveWebhookTargetWithAuthOrReject, + resolveWebhookTargetWithAuthOrRejectSync, + resolveWebhookTargets, + withResolvedWebhookRequestPipeline, + type RegisterWebhookPluginRouteOptions, + type RegisterWebhookTargetOptions, + type RegisteredWebhookTarget, + type WebhookTargetMatchResult, +} from "./webhook-targets.js"; +export { normalizeWebhookPath, resolveWebhookPath } from "./webhook-path.js"; diff --git a/src/plugin-sdk/zalo.ts b/src/plugin-sdk/zalo.ts index 21a5dd09b89..9b6e64bef34 100644 --- a/src/plugin-sdk/zalo.ts +++ b/src/plugin-sdk/zalo.ts @@ -34,9 +34,9 @@ export type { ChannelStatusIssue, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; export { logTypingFailure } from "../channels/logging.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; +export { createTypingCallbacks } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { resolveDefaultGroupPolicy, @@ -44,13 +44,13 @@ export { warnMissingProviderGroupPolicyFallbackOnce, } from "../config/runtime-group-policy.js"; export type { GroupPolicy, MarkdownTableMode } from "../config/types.js"; -export type { SecretInput } from "../config/types.secrets.js"; +export type { SecretInput } from "./secret-input.js"; export { + buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; +} from "./secret-input.js"; export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; export { waitForAbortSignal } from "../infra/abort-signal.js"; export { createDedupeCache } from "../infra/dedupe.js"; @@ -72,8 +72,7 @@ export { resolveChannelAccountConfigBasePath } from "./config-paths.js"; export { evaluateSenderGroupAccess } from "./group-access.js"; export type { SenderGroupAccessDecision } from "./group-access.js"; export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { buildChannelSendResult } from "./channel-send-result.js"; export type { OutboundReplyPayload } from "./reply-payload.js"; export { @@ -90,25 +89,21 @@ export { export { chunkTextForOutbound } from "./text-chunking.js"; export { extractToolSend } from "./tool-send.js"; export { + applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, + readJsonWebhookBodyOrReject, + registerWebhookTarget, + registerWebhookTargetWithPluginRoute, + resolveSingleWebhookTarget, + resolveWebhookPath, + resolveWebhookTargetWithAuthOrRejectSync, + resolveWebhookTargets, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, -} from "./webhook-memory-guards.js"; -export { resolveWebhookPath } from "./webhook-path.js"; -export { - applyBasicWebhookRequestGuards, - readJsonWebhookBodyOrReject, -} from "./webhook-request-guards.js"; + withResolvedWebhookRequestPipeline, +} from "./webhook-ingress.js"; export type { RegisterWebhookPluginRouteOptions, RegisterWebhookTargetOptions, -} from "./webhook-targets.js"; -export { - registerWebhookTarget, - registerWebhookTargetWithPluginRoute, - resolveWebhookTargetWithAuthOrRejectSync, - resolveSingleWebhookTarget, - resolveWebhookTargets, - withResolvedWebhookRequestPipeline, -} from "./webhook-targets.js"; +} from "./webhook-ingress.js"; diff --git a/src/plugin-sdk/zalouser.ts b/src/plugin-sdk/zalouser.ts index e7fb506f227..a88e62600f4 100644 --- a/src/plugin-sdk/zalouser.ts +++ b/src/plugin-sdk/zalouser.ts @@ -1,10 +1,7 @@ // Narrow plugin-sdk surface for the bundled zalouser plugin. // Keep this list additive and scoped to symbols used under extensions/zalouser. -import { - createOptionalChannelSetupAdapter, - createOptionalChannelSetupWizard, -} from "./optional-channel-setup.js"; +import { createOptionalChannelSetupSurface } from "./channel-setup.js"; export type { ReplyPayload } from "../auto-reply/types.js"; export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; @@ -36,8 +33,8 @@ export type { ChannelStatusIssue, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; +export { createTypingCallbacks } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { @@ -63,8 +60,7 @@ export { resolveSenderScopedGroupPolicy, } from "./group-access.js"; export { loadOutboundMediaFromUrl } from "./outbound-media.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; export { buildChannelSendResult } from "./channel-send-result.js"; export type { OutboundReplyPayload } from "./reply-payload.js"; export { @@ -79,16 +75,12 @@ export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; export { buildBaseAccountStatusSnapshot } from "./status-helpers.js"; export { chunkTextForOutbound } from "./text-chunking.js"; -export const zalouserSetupAdapter = createOptionalChannelSetupAdapter({ +const zalouserSetup = createOptionalChannelSetupSurface({ channel: "zalouser", label: "Zalo Personal", npmSpec: "@openclaw/zalouser", docsPath: "/channels/zalouser", }); -export const zalouserSetupWizard = createOptionalChannelSetupWizard({ - channel: "zalouser", - label: "Zalo Personal", - npmSpec: "@openclaw/zalouser", - docsPath: "/channels/zalouser", -}); +export const zalouserSetupAdapter = zalouserSetup.setupAdapter; +export const zalouserSetupWizard = zalouserSetup.setupWizard; diff --git a/src/plugins/provider-onboarding-config.ts b/src/plugins/provider-onboarding-config.ts index 9e70eaac192..cd86f9e52b5 100644 --- a/src/plugins/provider-onboarding-config.ts +++ b/src/plugins/provider-onboarding-config.ts @@ -18,6 +18,38 @@ function extractAgentDefaultModelFallbacks(model: unknown): string[] | undefined return Array.isArray(fallbacks) ? fallbacks.map((v) => String(v)) : undefined; } +export type AgentModelAliasEntry = + | string + | { + modelRef: string; + alias?: string; + }; + +function normalizeAgentModelAliasEntry(entry: AgentModelAliasEntry): { + modelRef: string; + alias?: string; +} { + if (typeof entry === "string") { + return { modelRef: entry }; + } + return entry; +} + +export function withAgentModelAliases( + existing: Record | undefined, + aliases: readonly AgentModelAliasEntry[], +): Record { + const next = { ...existing }; + for (const entry of aliases) { + const normalized = normalizeAgentModelAliasEntry(entry); + next[normalized.modelRef] = { + ...next[normalized.modelRef], + ...(normalized.alias ? { alias: next[normalized.modelRef]?.alias ?? normalized.alias } : {}), + }; + } + return next; +} + export function applyOnboardAuthAgentModelsAndProviders( cfg: OpenClawConfig, params: { @@ -117,6 +149,56 @@ export function applyProviderConfigWithDefaultModel( }); } +export function applyProviderConfigWithDefaultModelPreset( + cfg: OpenClawConfig, + params: { + providerId: string; + api: ModelApi; + baseUrl: string; + defaultModel: ModelDefinitionConfig; + defaultModelId?: string; + aliases?: readonly AgentModelAliasEntry[]; + primaryModelRef?: string; + }, +): OpenClawConfig { + const next = applyProviderConfigWithDefaultModel(cfg, { + agentModels: withAgentModelAliases(cfg.agents?.defaults?.models, params.aliases ?? []), + providerId: params.providerId, + api: params.api, + baseUrl: params.baseUrl, + defaultModel: params.defaultModel, + defaultModelId: params.defaultModelId, + }); + return params.primaryModelRef + ? applyAgentDefaultModelPrimary(next, params.primaryModelRef) + : next; +} + +export function applyProviderConfigWithDefaultModelsPreset( + cfg: OpenClawConfig, + params: { + providerId: string; + api: ModelApi; + baseUrl: string; + defaultModels: ModelDefinitionConfig[]; + defaultModelId?: string; + aliases?: readonly AgentModelAliasEntry[]; + primaryModelRef?: string; + }, +): OpenClawConfig { + const next = applyProviderConfigWithDefaultModels(cfg, { + agentModels: withAgentModelAliases(cfg.agents?.defaults?.models, params.aliases ?? []), + providerId: params.providerId, + api: params.api, + baseUrl: params.baseUrl, + defaultModels: params.defaultModels, + defaultModelId: params.defaultModelId, + }); + return params.primaryModelRef + ? applyAgentDefaultModelPrimary(next, params.primaryModelRef) + : next; +} + export function applyProviderConfigWithModelCatalog( cfg: OpenClawConfig, params: { @@ -149,6 +231,29 @@ export function applyProviderConfigWithModelCatalog( }); } +export function applyProviderConfigWithModelCatalogPreset( + cfg: OpenClawConfig, + params: { + providerId: string; + api: ModelApi; + baseUrl: string; + catalogModels: ModelDefinitionConfig[]; + aliases?: readonly AgentModelAliasEntry[]; + primaryModelRef?: string; + }, +): OpenClawConfig { + const next = applyProviderConfigWithModelCatalog(cfg, { + agentModels: withAgentModelAliases(cfg.agents?.defaults?.models, params.aliases ?? []), + providerId: params.providerId, + api: params.api, + baseUrl: params.baseUrl, + catalogModels: params.catalogModels, + }); + return params.primaryModelRef + ? applyAgentDefaultModelPrimary(next, params.primaryModelRef) + : next; +} + type ProviderModelMergeState = { providers: Record; existingProvider?: ModelProviderConfig;