From 80eb91d9e759d696556babf31ff64d576635ee4b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 19:35:38 +0000 Subject: [PATCH] refactor(plugin-sdk): add shared helper utilities --- src/plugin-sdk/agent-media-payload.ts | 24 +++++++++++ src/plugin-sdk/index.ts | 9 ++++ src/plugin-sdk/provider-auth-result.ts | 47 +++++++++++++++++++++ src/plugin-sdk/status-helpers.ts | 57 ++++++++++++++++++++++++++ src/plugin-sdk/webhook-path.ts | 31 ++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 src/plugin-sdk/agent-media-payload.ts create mode 100644 src/plugin-sdk/provider-auth-result.ts create mode 100644 src/plugin-sdk/status-helpers.ts create mode 100644 src/plugin-sdk/webhook-path.ts diff --git a/src/plugin-sdk/agent-media-payload.ts b/src/plugin-sdk/agent-media-payload.ts new file mode 100644 index 0000000000..98d12a8420 --- /dev/null +++ b/src/plugin-sdk/agent-media-payload.ts @@ -0,0 +1,24 @@ +export type AgentMediaPayload = { + MediaPath?: string; + MediaType?: string; + MediaUrl?: string; + MediaPaths?: string[]; + MediaUrls?: string[]; + MediaTypes?: string[]; +}; + +export function buildAgentMediaPayload( + mediaList: Array<{ path: string; contentType?: string | null }>, +): AgentMediaPayload { + const first = mediaList[0]; + const mediaPaths = mediaList.map((media) => media.path); + const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[]; + return { + MediaPath: first?.path, + MediaType: first?.contentType ?? undefined, + MediaUrl: first?.path, + MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined, + MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined, + MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined, + }; +} diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 48ad88aacc..662a4fec95 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -83,6 +83,15 @@ export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js"; export type { FileLockHandle, FileLockOptions } from "./file-lock.js"; export { acquireFileLock, withFileLock } from "./file-lock.js"; +export { normalizeWebhookPath, resolveWebhookPath } from "./webhook-path.js"; +export type { AgentMediaPayload } from "./agent-media-payload.js"; +export { buildAgentMediaPayload } from "./agent-media-payload.js"; +export { + buildBaseChannelStatusSummary, + collectStatusIssuesFromLastError, + createDefaultChannelRuntimeState, +} from "./status-helpers.js"; +export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; export type { ChannelDock } from "../channels/dock.js"; export { getChatChannelMeta } from "../channels/registry.js"; export type { diff --git a/src/plugin-sdk/provider-auth-result.ts b/src/plugin-sdk/provider-auth-result.ts new file mode 100644 index 0000000000..c16c23cc15 --- /dev/null +++ b/src/plugin-sdk/provider-auth-result.ts @@ -0,0 +1,47 @@ +import type { AuthProfileCredential } from "../agents/auth-profiles/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ProviderAuthResult } from "../plugins/types.js"; + +export function buildOauthProviderAuthResult(params: { + providerId: string; + defaultModel: string; + access: string; + refresh?: string | null; + expires?: number | null; + email?: string | null; + profilePrefix?: string; + credentialExtra?: Record; + configPatch?: Partial; + notes?: string[]; +}): ProviderAuthResult { + const email = params.email ?? undefined; + const profilePrefix = params.profilePrefix ?? params.providerId; + const profileId = `${profilePrefix}:${email ?? "default"}`; + + const credential: AuthProfileCredential = { + type: "oauth", + provider: params.providerId, + access: params.access, + ...(params.refresh ? { refresh: params.refresh } : {}), + ...(Number.isFinite(params.expires) ? { expires: params.expires as number } : {}), + ...(email ? { email } : {}), + ...params.credentialExtra, + } as AuthProfileCredential; + + return { + profiles: [{ profileId, credential }], + configPatch: + params.configPatch ?? + ({ + agents: { + defaults: { + models: { + [params.defaultModel]: {}, + }, + }, + }, + } as Partial), + defaultModel: params.defaultModel, + notes: params.notes, + }; +} diff --git a/src/plugin-sdk/status-helpers.ts b/src/plugin-sdk/status-helpers.ts new file mode 100644 index 0000000000..945dca1bcb --- /dev/null +++ b/src/plugin-sdk/status-helpers.ts @@ -0,0 +1,57 @@ +import type { ChannelStatusIssue } from "../channels/plugins/types.js"; + +export function createDefaultChannelRuntimeState>( + accountId: string, + extra?: T, +): { + accountId: string; + running: false; + lastStartAt: null; + lastStopAt: null; + lastError: null; +} & T { + return { + accountId, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + ...(extra ?? ({} as T)), + }; +} + +export function buildBaseChannelStatusSummary(snapshot: { + configured?: boolean | null; + running?: boolean | null; + lastStartAt?: number | null; + lastStopAt?: number | null; + lastError?: string | null; +}) { + return { + configured: snapshot.configured ?? false, + running: snapshot.running ?? false, + lastStartAt: snapshot.lastStartAt ?? null, + lastStopAt: snapshot.lastStopAt ?? null, + lastError: snapshot.lastError ?? null, + }; +} + +export function collectStatusIssuesFromLastError( + channel: string, + accounts: Array<{ accountId: string; lastError?: unknown }>, +): ChannelStatusIssue[] { + return accounts.flatMap((account) => { + const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; + if (!lastError) { + return []; + } + return [ + { + channel, + accountId: account.accountId, + kind: "runtime", + message: `Channel error: ${lastError}`, + }, + ]; + }); +} diff --git a/src/plugin-sdk/webhook-path.ts b/src/plugin-sdk/webhook-path.ts new file mode 100644 index 0000000000..41e4bd0ba9 --- /dev/null +++ b/src/plugin-sdk/webhook-path.ts @@ -0,0 +1,31 @@ +export function normalizeWebhookPath(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) { + return "/"; + } + const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + if (withSlash.length > 1 && withSlash.endsWith("/")) { + return withSlash.slice(0, -1); + } + return withSlash; +} + +export function resolveWebhookPath(params: { + webhookPath?: string; + webhookUrl?: string; + defaultPath?: string | null; +}): string | null { + const trimmedPath = params.webhookPath?.trim(); + if (trimmedPath) { + return normalizeWebhookPath(trimmedPath); + } + if (params.webhookUrl?.trim()) { + try { + const parsed = new URL(params.webhookUrl); + return normalizeWebhookPath(parsed.pathname || "/"); + } catch { + return null; + } + } + return params.defaultPath ?? null; +}