diff --git a/extensions/acpx/runtime-api.ts b/extensions/acpx/runtime-api.ts index 9a019cdd0e6..7a8a555a9a7 100644 --- a/extensions/acpx/runtime-api.ts +++ b/extensions/acpx/runtime-api.ts @@ -1 +1,38 @@ -export * from "../../src/plugin-sdk/acpx.js"; +export type { AcpRuntimeErrorCode } from "openclaw/plugin-sdk/acp-runtime"; +export { + AcpRuntimeError, + registerAcpRuntimeBackend, + unregisterAcpRuntimeBackend, +} from "openclaw/plugin-sdk/acp-runtime"; +export type { + AcpRuntime, + AcpRuntimeCapabilities, + AcpRuntimeDoctorReport, + AcpRuntimeEnsureInput, + AcpRuntimeEvent, + AcpRuntimeHandle, + AcpRuntimeStatus, + AcpRuntimeTurnInput, + AcpSessionUpdateTag, +} from "openclaw/plugin-sdk/acp-runtime"; +export type { + OpenClawPluginApi, + OpenClawPluginConfigSchema, + OpenClawPluginService, + OpenClawPluginServiceContext, + PluginLogger, +} from "openclaw/plugin-sdk/core"; +export type { + WindowsSpawnProgram, + WindowsSpawnProgramCandidate, + WindowsSpawnResolution, +} from "openclaw/plugin-sdk/windows-spawn"; +export { + applyWindowsSpawnProgramPolicy, + materializeWindowsSpawnProgram, + resolveWindowsSpawnProgramCandidate, +} from "openclaw/plugin-sdk/windows-spawn"; +export { + listKnownProviderAuthEnvVarNames, + omitEnvKeysCaseInsensitive, +} from "openclaw/plugin-sdk/provider-env-vars"; diff --git a/extensions/firecrawl/src/firecrawl-client.ts b/extensions/firecrawl/src/firecrawl-client.ts index 565e1d6aac3..fa38c5bdabe 100644 --- a/extensions/firecrawl/src/firecrawl-client.ts +++ b/extensions/firecrawl/src/firecrawl-client.ts @@ -1,11 +1,10 @@ import { markdownToText, truncateText } from "openclaw/plugin-sdk/agent-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { withTrustedWebToolsEndpoint } from "openclaw/plugin-sdk/provider-web-search"; import { DEFAULT_CACHE_TTL_MINUTES, normalizeCacheKey, + postTrustedWebToolsJson, readCache, - readResponseText, resolveCacheTtlMs, writeCache, } from "openclaw/plugin-sdk/provider-web-search"; @@ -29,7 +28,6 @@ const SCRAPE_CACHE = new Map< >(); const DEFAULT_SEARCH_COUNT = 5; const DEFAULT_SCRAPE_MAX_CHARS = 50_000; -const DEFAULT_ERROR_MAX_BYTES = 64_000; type FirecrawlSearchItem = { title: string; @@ -88,51 +86,6 @@ function resolveSiteName(urlRaw: string): string | undefined { } } -async function postFirecrawlJson(params: { - baseUrl: string; - pathname: "/v2/search" | "/v2/scrape"; - apiKey: string; - body: Record; - timeoutSeconds: number; - errorLabel: string; -}): Promise> { - const endpoint = resolveEndpoint(params.baseUrl, params.pathname); - return await withTrustedWebToolsEndpoint( - { - url: endpoint, - timeoutSeconds: params.timeoutSeconds, - init: { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${params.apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(params.body), - }, - }, - async ({ response }) => { - if (!response.ok) { - const detail = await readResponseText(response, { maxBytes: DEFAULT_ERROR_MAX_BYTES }); - throw new Error( - `${params.errorLabel} API error (${response.status}): ${detail.text || response.statusText}`, - ); - } - const payload = (await response.json()) as Record; - if (payload.success === false) { - const error = - typeof payload.error === "string" - ? payload.error - : typeof payload.message === "string" - ? payload.message - : "unknown error"; - throw new Error(`${params.errorLabel} API error: ${error}`); - } - return payload; - }, - ); -} - function resolveSearchItems(payload: Record): FirecrawlSearchItem[] { const candidates = [ payload.data, @@ -279,14 +232,28 @@ export async function runFirecrawlSearch( } const start = Date.now(); - const payload = await postFirecrawlJson({ - baseUrl, - pathname: "/v2/search", - apiKey, - body, - timeoutSeconds, - errorLabel: "Firecrawl Search", - }); + const payload = await postTrustedWebToolsJson( + { + url: resolveEndpoint(baseUrl, "/v2/search"), + timeoutSeconds, + apiKey, + body, + errorLabel: "Firecrawl Search", + }, + async (response) => { + const payload = (await response.json()) as Record; + if (payload.success === false) { + const error = + typeof payload.error === "string" + ? payload.error + : typeof payload.message === "string" + ? payload.message + : "unknown error"; + throw new Error(`Firecrawl Search API error: ${error}`); + } + return payload; + }, + ); const result = buildSearchPayload({ query: params.query, provider: "firecrawl", @@ -409,22 +376,24 @@ export async function runFirecrawlScrape( return { ...cached.value, cached: true }; } - const payload = await postFirecrawlJson({ - baseUrl, - pathname: "/v2/scrape", - apiKey, - timeoutSeconds, - errorLabel: "Firecrawl", - body: { - url: params.url, - formats: ["markdown"], - onlyMainContent, - timeout: timeoutSeconds * 1000, - maxAge: maxAgeMs, - proxy, - storeInCache, + const payload = await postTrustedWebToolsJson( + { + url: resolveEndpoint(baseUrl, "/v2/scrape"), + timeoutSeconds, + apiKey, + errorLabel: "Firecrawl", + body: { + url: params.url, + formats: ["markdown"], + onlyMainContent, + timeout: timeoutSeconds * 1000, + maxAge: maxAgeMs, + proxy, + storeInCache, + }, }, - }); + async (response) => (await response.json()) as Record, + ); const result = parseFirecrawlScrapePayload({ payload, url: params.url, diff --git a/extensions/google/runtime-api.ts b/extensions/google/runtime-api.ts index 60e25c7303e..9b2b8047998 100644 --- a/extensions/google/runtime-api.ts +++ b/extensions/google/runtime-api.ts @@ -1 +1 @@ -export * from "../../src/plugin-sdk/google.js"; +export { normalizeGoogleModelId, parseGeminiAuth } from "openclaw/plugin-sdk/provider-google"; diff --git a/extensions/line/src/channel-shared.ts b/extensions/line/src/channel-shared.ts new file mode 100644 index 00000000000..593824f3070 --- /dev/null +++ b/extensions/line/src/channel-shared.ts @@ -0,0 +1,66 @@ +import type { ChannelPlugin } from "../api.js"; +import { + resolveLineAccount, + type OpenClawConfig, + type ResolvedLineAccount, +} from "../runtime-api.js"; +import { lineConfigAdapter } from "./config-adapter.js"; +import { LineChannelConfigSchema } from "./config-schema.js"; + +export const lineChannelMeta = { + id: "line", + label: "LINE", + selectionLabel: "LINE (Messaging API)", + detailLabel: "LINE Bot", + docsPath: "/channels/line", + docsLabel: "line", + blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.", + systemImage: "message.fill", +} as const; + +export const lineChannelPluginCommon = { + meta: { + ...lineChannelMeta, + quickstartAllowFrom: true, + }, + capabilities: { + chatTypes: ["direct", "group"], + reactions: false, + threads: false, + media: true, + nativeCommands: false, + blockStreaming: true, + }, + reload: { configPrefixes: ["channels.line"] }, + configSchema: LineChannelConfigSchema, + config: { + ...lineConfigAdapter, + isConfigured: (account: ResolvedLineAccount) => + Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), + describeAccount: (account: ResolvedLineAccount) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), + tokenSource: account.tokenSource ?? undefined, + }), + }, +} satisfies Pick< + ChannelPlugin, + "meta" | "capabilities" | "reload" | "configSchema" | "config" +>; + +export function isLineConfigured(cfg: OpenClawConfig, accountId: string): boolean { + const resolved = resolveLineAccount({ cfg, accountId }); + return Boolean(resolved.channelAccessToken.trim() && resolved.channelSecret.trim()); +} + +export function parseLineAllowFromId(raw: string): string | null { + const trimmed = raw.trim().replace(/^line:(?:user:)?/i, ""); + if (!/^U[a-f0-9]{32}$/i.test(trimmed)) { + return null; + } + return trimmed; +} + +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../runtime-api.js"; diff --git a/extensions/line/src/channel.setup.ts b/extensions/line/src/channel.setup.ts index bae717a205d..cbd36f44446 100644 --- a/extensions/line/src/channel.setup.ts +++ b/extensions/line/src/channel.setup.ts @@ -1,52 +1,11 @@ -import { - buildChannelConfigSchema, - LineConfigSchema, - type ChannelPlugin, - type ResolvedLineAccount, -} from "../api.js"; -import { lineConfigAdapter } from "./config-adapter.js"; +import { type ChannelPlugin, type ResolvedLineAccount } from "../api.js"; +import { lineChannelPluginCommon } from "./channel-shared.js"; import { lineSetupAdapter } from "./setup-core.js"; import { lineSetupWizard } from "./setup-surface.js"; -const meta = { - id: "line", - label: "LINE", - selectionLabel: "LINE (Messaging API)", - detailLabel: "LINE Bot", - docsPath: "/channels/line", - docsLabel: "line", - blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.", - systemImage: "message.fill", -} as const; - export const lineSetupPlugin: ChannelPlugin = { id: "line", - meta: { - ...meta, - quickstartAllowFrom: true, - }, - capabilities: { - chatTypes: ["direct", "group"], - reactions: false, - threads: false, - media: true, - nativeCommands: false, - blockStreaming: true, - }, - reload: { configPrefixes: ["channels.line"] }, - configSchema: buildChannelConfigSchema(LineConfigSchema), - config: { - ...lineConfigAdapter, - isConfigured: (account) => - Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), - describeAccount: (account) => ({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), - tokenSource: account.tokenSource ?? undefined, - }), - }, + ...lineChannelPluginCommon, setupWizard: lineSetupWizard, setup: lineSetupAdapter, }; diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index d983d2a0172..54cd54ff7bf 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -9,12 +9,10 @@ import { } from "openclaw/plugin-sdk/channel-runtime"; import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload"; import { - buildChannelConfigSchema, buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary, clearAccountEntryFields, DEFAULT_ACCOUNT_ID, - LineConfigSchema, processLineMessage, type ChannelPlugin, type ChannelStatusIssue, @@ -23,24 +21,12 @@ import { type OpenClawConfig, type ResolvedLineAccount, } from "../api.js"; -import { lineConfigAdapter } from "./config-adapter.js"; +import { lineChannelPluginCommon } from "./channel-shared.js"; import { resolveLineGroupRequireMention } from "./group-policy.js"; import { getLineRuntime } from "./runtime.js"; import { lineSetupAdapter } from "./setup-core.js"; import { lineSetupWizard } from "./setup-surface.js"; -// LINE channel metadata -const meta = { - id: "line", - label: "LINE", - selectionLabel: "LINE (Messaging API)", - detailLabel: "LINE Bot", - docsPath: "/channels/line", - docsLabel: "line", - blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.", - systemImage: "message.fill", -}; - const resolveLineDmPolicy = createScopedDmSecurityResolver({ channelKey: "line", resolvePolicy: (account) => account.config.dmPolicy, @@ -63,10 +49,7 @@ const collectLineSecurityWarnings = export const linePlugin: ChannelPlugin = { id: "line", - meta: { - ...meta, - quickstartAllowFrom: true, - }, + ...lineChannelPluginCommon, pairing: createTextPairingAdapter({ idLabel: "lineUserId", message: "OpenClaw: your access has been approved.", @@ -83,29 +66,7 @@ export const linePlugin: ChannelPlugin = { }); }, }), - capabilities: { - chatTypes: ["direct", "group"], - reactions: false, - threads: false, - media: true, - nativeCommands: false, - blockStreaming: true, - }, - reload: { configPrefixes: ["channels.line"] }, - configSchema: buildChannelConfigSchema(LineConfigSchema), setupWizard: lineSetupWizard, - config: { - ...lineConfigAdapter, - isConfigured: (account) => - Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), - describeAccount: (account) => ({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), - tokenSource: account.tokenSource ?? undefined, - }), - }, security: { resolveDmPolicy: resolveLineDmPolicy, collectWarnings: collectLineSecurityWarnings, diff --git a/extensions/lobster/runtime-api.ts b/extensions/lobster/runtime-api.ts index 24898e04cf5..d883e0853b3 100644 --- a/extensions/lobster/runtime-api.ts +++ b/extensions/lobster/runtime-api.ts @@ -1 +1,12 @@ -export * from "../../src/plugin-sdk/lobster.js"; +export { definePluginEntry } from "openclaw/plugin-sdk/core"; +export type { + AnyAgentTool, + OpenClawPluginApi, + OpenClawPluginToolContext, + OpenClawPluginToolFactory, +} from "openclaw/plugin-sdk/core"; +export { + applyWindowsSpawnProgramPolicy, + materializeWindowsSpawnProgram, + resolveWindowsSpawnProgramCandidate, +} from "openclaw/plugin-sdk/windows-spawn"; diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index 1aaee387fc8..865936cb6ff 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -3,3 +3,4 @@ // matrix-js-sdk during plain runtime-api import. export * from "./src/auth-precedence.js"; export * from "./helper-api.js"; +export * from "./thread-bindings-runtime.js"; diff --git a/extensions/matrix/src/matrix/format.ts b/extensions/matrix/src/matrix/format.ts index 31bddcc5292..efb81ebff2a 100644 --- a/extensions/matrix/src/matrix/format.ts +++ b/extensions/matrix/src/matrix/format.ts @@ -1,4 +1,5 @@ import MarkdownIt from "markdown-it"; +import { isAutoLinkedFileRef } from "openclaw/plugin-sdk/text-runtime"; const md = new MarkdownIt({ html: false, @@ -10,38 +11,6 @@ const md = new MarkdownIt({ md.enable("strikethrough"); const { escapeHtml } = md.utils; - -/** - * Keep bare file references like README.md from becoming external http:// links. - * Telegram already hardens this path; Matrix should not turn common code/docs - * filenames into clickable registrar-style URLs either. - */ -const FILE_EXTENSIONS_WITH_TLD = new Set(["md", "go", "py", "pl", "sh", "am", "at", "be", "cc"]); - -function isAutoLinkedFileRef(href: string, label: string): boolean { - const stripped = href.replace(/^https?:\/\//i, ""); - if (stripped !== label) { - return false; - } - const dotIndex = label.lastIndexOf("."); - if (dotIndex < 1) { - return false; - } - const ext = label.slice(dotIndex + 1).toLowerCase(); - if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) { - return false; - } - const segments = label.split("/"); - if (segments.length > 1) { - for (let i = 0; i < segments.length - 1; i += 1) { - if (segments[i]?.includes(".")) { - return false; - } - } - } - return true; -} - function shouldSuppressAutoLink( tokens: Parameters>[0], idx: number, diff --git a/extensions/matrix/src/matrix/thread-bindings-shared.ts b/extensions/matrix/src/matrix/thread-bindings-shared.ts index f8c9c2b9e3f..7b5adb5eeda 100644 --- a/extensions/matrix/src/matrix/thread-bindings-shared.ts +++ b/extensions/matrix/src/matrix/thread-bindings-shared.ts @@ -1,3 +1,4 @@ +import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/channel-runtime"; import type { BindingTargetKind, SessionBindingRecord, @@ -74,32 +75,7 @@ export function resolveEffectiveBindingExpiry(params: { expiresAt?: number; reason?: "idle-expired" | "max-age-expired"; } { - const idleTimeoutMs = - typeof params.record.idleTimeoutMs === "number" - ? Math.max(0, Math.floor(params.record.idleTimeoutMs)) - : params.defaultIdleTimeoutMs; - const maxAgeMs = - typeof params.record.maxAgeMs === "number" - ? Math.max(0, Math.floor(params.record.maxAgeMs)) - : params.defaultMaxAgeMs; - const inactivityExpiresAt = - idleTimeoutMs > 0 - ? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs - : undefined; - const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined; - - if (inactivityExpiresAt != null && maxAgeExpiresAt != null) { - return inactivityExpiresAt <= maxAgeExpiresAt - ? { expiresAt: inactivityExpiresAt, reason: "idle-expired" } - : { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" }; - } - if (inactivityExpiresAt != null) { - return { expiresAt: inactivityExpiresAt, reason: "idle-expired" }; - } - if (maxAgeExpiresAt != null) { - return { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" }; - } - return {}; + return resolveThreadBindingLifecycle(params); } export function toSessionBindingRecord( diff --git a/extensions/tavily/src/tavily-client.ts b/extensions/tavily/src/tavily-client.ts index 8308f8b8772..c57f5850af3 100644 --- a/extensions/tavily/src/tavily-client.ts +++ b/extensions/tavily/src/tavily-client.ts @@ -1,10 +1,9 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { withTrustedWebToolsEndpoint } from "openclaw/plugin-sdk/provider-web-search"; import { DEFAULT_CACHE_TTL_MINUTES, normalizeCacheKey, + postTrustedWebToolsJson, readCache, - readResponseText, resolveCacheTtlMs, writeCache, } from "openclaw/plugin-sdk/provider-web-search"; @@ -26,7 +25,6 @@ const EXTRACT_CACHE = new Map< { value: Record; expiresAt: number; insertedAt: number } >(); const DEFAULT_SEARCH_COUNT = 5; -const DEFAULT_ERROR_MAX_BYTES = 64_000; export type TavilySearchParams = { cfg?: OpenClawConfig; @@ -67,41 +65,6 @@ function resolveEndpoint(baseUrl: string, pathname: string): string { } } -async function postTavilyJson(params: { - baseUrl: string; - pathname: string; - apiKey: string; - body: Record; - timeoutSeconds: number; - errorLabel: string; -}): Promise> { - const endpoint = resolveEndpoint(params.baseUrl, params.pathname); - return await withTrustedWebToolsEndpoint( - { - url: endpoint, - timeoutSeconds: params.timeoutSeconds, - init: { - method: "POST", - headers: { - Accept: "application/json", - Authorization: `Bearer ${params.apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(params.body), - }, - }, - async ({ response }) => { - if (!response.ok) { - const detail = await readResponseText(response, { maxBytes: DEFAULT_ERROR_MAX_BYTES }); - throw new Error( - `${params.errorLabel} API error (${response.status}): ${detail.text || response.statusText}`, - ); - } - return (await response.json()) as Record; - }, - ); -} - export async function runTavilySearch( params: TavilySearchParams, ): Promise> { @@ -149,14 +112,16 @@ export async function runTavilySearch( if (params.excludeDomains?.length) body.exclude_domains = params.excludeDomains; const start = Date.now(); - const payload = await postTavilyJson({ - baseUrl, - pathname: "/search", - apiKey, - body, - timeoutSeconds, - errorLabel: "Tavily Search", - }); + const payload = await postTrustedWebToolsJson( + { + url: resolveEndpoint(baseUrl, "/search"), + timeoutSeconds, + apiKey, + body, + errorLabel: "Tavily Search", + }, + async (response) => (await response.json()) as Record, + ); const rawResults = Array.isArray(payload.results) ? payload.results : []; const results = rawResults.map((r: Record) => ({ @@ -228,14 +193,16 @@ export async function runTavilyExtract( if (params.includeImages) body.include_images = true; const start = Date.now(); - const payload = await postTavilyJson({ - baseUrl, - pathname: "/extract", - apiKey, - body, - timeoutSeconds, - errorLabel: "Tavily Extract", - }); + const payload = await postTrustedWebToolsJson( + { + url: resolveEndpoint(baseUrl, "/extract"), + timeoutSeconds, + apiKey, + body, + errorLabel: "Tavily Extract", + }, + async (response) => (await response.json()) as Record, + ); const rawResults = Array.isArray(payload.results) ? payload.results : []; const results = rawResults.map((r: Record) => ({ @@ -282,5 +249,5 @@ export async function runTavilyExtract( } export const __testing = { - postTavilyJson, + resolveEndpoint, }; diff --git a/extensions/telegram/src/format.ts b/extensions/telegram/src/format.ts index a9a10965243..4d14f179b2f 100644 --- a/extensions/telegram/src/format.ts +++ b/extensions/telegram/src/format.ts @@ -1,6 +1,8 @@ import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { chunkMarkdownIR, + FILE_REF_EXTENSIONS_WITH_TLD, + isAutoLinkedFileRef, markdownToIR, type MarkdownLinkSpan, type MarkdownIR, @@ -31,44 +33,6 @@ function escapeHtmlAttr(text: string): string { * * Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io) */ -const FILE_EXTENSIONS_WITH_TLD = new Set([ - "md", // Markdown (Moldova) - very common in repos - "go", // Go language - common in Go projects - "py", // Python (Paraguay) - common in Python projects - "pl", // Perl (Poland) - common in Perl projects - "sh", // Shell (Saint Helena) - common for scripts - "am", // Automake files (Armenia) - "at", // Assembly (Austria) - "be", // Backend files (Belgium) - "cc", // C++ source (Cocos Islands) -]); - -/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */ -function isAutoLinkedFileRef(href: string, label: string): boolean { - const stripped = href.replace(/^https?:\/\//i, ""); - if (stripped !== label) { - return false; - } - const dotIndex = label.lastIndexOf("."); - if (dotIndex < 1) { - return false; - } - const ext = label.slice(dotIndex + 1).toLowerCase(); - if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) { - return false; - } - // Reject if any path segment before the filename contains a dot (looks like a domain) - const segments = label.split("/"); - if (segments.length > 1) { - for (let i = 0; i < segments.length - 1; i++) { - if (segments[i].includes(".")) { - return false; - } - } - } - return true; -} - function buildTelegramLink(link: MarkdownLinkSpan, text: string) { const href = link.href.trim(); if (!href) { @@ -139,7 +103,7 @@ function escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|"); +const FILE_EXTENSIONS_PATTERN = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|"); const AUTO_LINKED_ANCHOR_PATTERN = /]*>\1<\/a>/gi; const FILE_REFERENCE_PATTERN = new RegExp( `(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, diff --git a/extensions/telegram/src/thread-bindings.ts b/extensions/telegram/src/thread-bindings.ts index 8b7be041197..0078c3362e6 100644 --- a/extensions/telegram/src/thread-bindings.ts +++ b/extensions/telegram/src/thread-bindings.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { resolveThreadBindingConversationIdFromBindingId } from "openclaw/plugin-sdk/channel-runtime"; +import { resolveThreadBindingEffectiveExpiresAt } from "openclaw/plugin-sdk/channel-runtime"; import { formatThreadBindingDurationLabel } from "openclaw/plugin-sdk/channel-runtime"; import { registerSessionBindingAdapter, @@ -115,32 +116,6 @@ function toTelegramTargetKind(raw: BindingTargetKind): TelegramBindingTargetKind return raw === "subagent" ? "subagent" : "acp"; } -function resolveEffectiveBindingExpiresAt(params: { - record: TelegramThreadBindingRecord; - defaultIdleTimeoutMs: number; - defaultMaxAgeMs: number; -}): number | undefined { - const idleTimeoutMs = - typeof params.record.idleTimeoutMs === "number" - ? Math.max(0, Math.floor(params.record.idleTimeoutMs)) - : params.defaultIdleTimeoutMs; - const maxAgeMs = - typeof params.record.maxAgeMs === "number" - ? Math.max(0, Math.floor(params.record.maxAgeMs)) - : params.defaultMaxAgeMs; - - const inactivityExpiresAt = - idleTimeoutMs > 0 - ? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs - : undefined; - const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined; - - if (inactivityExpiresAt != null && maxAgeExpiresAt != null) { - return Math.min(inactivityExpiresAt, maxAgeExpiresAt); - } - return inactivityExpiresAt ?? maxAgeExpiresAt; -} - function toSessionBindingRecord( record: TelegramThreadBindingRecord, defaults: { idleTimeoutMs: number; maxAgeMs: number }, @@ -159,7 +134,7 @@ function toSessionBindingRecord( }, status: "active", boundAt: record.boundAt, - expiresAt: resolveEffectiveBindingExpiresAt({ + expiresAt: resolveThreadBindingEffectiveExpiresAt({ record, defaultIdleTimeoutMs: defaults.idleTimeoutMs, defaultMaxAgeMs: defaults.maxAgeMs, diff --git a/extensions/xai/src/grok-web-search-provider.ts b/extensions/xai/src/grok-web-search-provider.ts index d9a6f0f8d46..705a8299917 100644 --- a/extensions/xai/src/grok-web-search-provider.ts +++ b/extensions/xai/src/grok-web-search-provider.ts @@ -5,12 +5,12 @@ import { DEFAULT_SEARCH_COUNT, getScopedCredentialValue, MAX_SEARCH_COUNT, - mergeScopedSearchConfig, readCachedSearchPayload, readConfiguredSecretString, readNumberParam, readProviderEnvValue, readStringParam, + mergeScopedSearchConfig, resolveProviderWebSearchPluginConfig, resolveSearchCacheTtlMs, resolveSearchCount, @@ -20,151 +20,24 @@ import { type SearchConfigRecord, type WebSearchProviderPlugin, type WebSearchProviderToolDefinition, - withTrustedWebSearchEndpoint, - wrapWebContent, writeCachedSearchPayload, } from "openclaw/plugin-sdk/provider-web-search"; +import { + buildXaiWebSearchPayload, + extractXaiWebSearchContent, + requestXaiWebSearch, + resolveXaiInlineCitations, + resolveXaiSearchConfig, + resolveXaiWebSearchModel, +} from "./web-search-shared.js"; -const XAI_API_ENDPOINT = "https://api.x.ai/v1/responses"; -const DEFAULT_GROK_MODEL = "grok-4-1-fast"; - -type GrokConfig = { - apiKey?: string; - model?: string; - inlineCitations?: boolean; -}; - -type GrokSearchResponse = { - output?: Array<{ - type?: string; - role?: string; - text?: string; - content?: Array<{ - type?: string; - text?: string; - annotations?: Array<{ - type?: string; - url?: string; - start_index?: number; - end_index?: number; - }>; - }>; - annotations?: Array<{ - type?: string; - url?: string; - start_index?: number; - end_index?: number; - }>; - }>; - output_text?: string; - citations?: string[]; - inline_citations?: Array<{ - start_index: number; - end_index: number; - url: string; - }>; -}; - -function resolveGrokConfig(searchConfig?: SearchConfigRecord): GrokConfig { - const grok = searchConfig?.grok; - return grok && typeof grok === "object" && !Array.isArray(grok) ? (grok as GrokConfig) : {}; -} - -function resolveGrokApiKey(grok?: GrokConfig): string | undefined { +function resolveGrokApiKey(grok?: Record): string | undefined { return ( readConfiguredSecretString(grok?.apiKey, "tools.web.search.grok.apiKey") ?? readProviderEnvValue(["XAI_API_KEY"]) ); } -function resolveGrokModel(grok?: GrokConfig): string { - const model = typeof grok?.model === "string" ? grok.model.trim() : ""; - return model || DEFAULT_GROK_MODEL; -} - -function resolveGrokInlineCitations(grok?: GrokConfig): boolean { - return grok?.inlineCitations === true; -} - -function extractGrokContent(data: GrokSearchResponse): { - text: string | undefined; - annotationCitations: string[]; -} { - for (const output of data.output ?? []) { - if (output.type === "message") { - for (const block of output.content ?? []) { - if (block.type === "output_text" && typeof block.text === "string" && block.text) { - const urls = (block.annotations ?? []) - .filter( - (annotation) => - annotation.type === "url_citation" && typeof annotation.url === "string", - ) - .map((annotation) => annotation.url as string); - return { text: block.text, annotationCitations: [...new Set(urls)] }; - } - } - } - if (output.type === "output_text" && typeof output.text === "string" && output.text) { - const urls = (Array.isArray(output.annotations) ? output.annotations : []) - .filter( - (annotation) => annotation.type === "url_citation" && typeof annotation.url === "string", - ) - .map((annotation) => annotation.url as string); - return { text: output.text, annotationCitations: [...new Set(urls)] }; - } - } - - return { - text: typeof data.output_text === "string" ? data.output_text : undefined, - annotationCitations: [], - }; -} - -async function runGrokSearch(params: { - query: string; - apiKey: string; - model: string; - timeoutSeconds: number; - inlineCitations: boolean; -}): Promise<{ - content: string; - citations: string[]; - inlineCitations?: GrokSearchResponse["inline_citations"]; -}> { - return withTrustedWebSearchEndpoint( - { - url: XAI_API_ENDPOINT, - timeoutSeconds: params.timeoutSeconds, - init: { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${params.apiKey}`, - }, - body: JSON.stringify({ - model: params.model, - input: [{ role: "user", content: params.query }], - tools: [{ type: "web_search" }], - }), - }, - }, - async (res) => { - if (!res.ok) { - const detail = await res.text(); - throw new Error(`xAI API error (${res.status}): ${detail || res.statusText}`); - } - - const data = (await res.json()) as GrokSearchResponse; - const { text, annotationCitations } = extractGrokContent(data); - return { - content: text ?? "No response", - citations: (data.citations ?? []).length > 0 ? data.citations! : annotationCitations, - inlineCitations: data.inline_citations, - }; - }, - ); -} - function createGrokSchema() { return Type.Object({ query: Type.String({ description: "Search query string." }), @@ -197,7 +70,7 @@ function createGrokToolDefinition( return unsupportedResponse; } - const grokConfig = resolveGrokConfig(searchConfig); + const grokConfig = resolveXaiSearchConfig(searchConfig); const apiKey = resolveGrokApiKey(grokConfig); if (!apiKey) { return { @@ -213,8 +86,8 @@ function createGrokToolDefinition( readNumberParam(params, "count", { integer: true }) ?? searchConfig?.maxResults ?? undefined; - const model = resolveGrokModel(grokConfig); - const inlineCitations = resolveGrokInlineCitations(grokConfig); + const model = resolveXaiWebSearchModel(searchConfig); + const inlineCitations = resolveXaiInlineCitations(searchConfig); const cacheKey = buildSearchCacheKey([ "grok", query, @@ -228,28 +101,22 @@ function createGrokToolDefinition( } const start = Date.now(); - const result = await runGrokSearch({ + const result = await requestXaiWebSearch({ query, apiKey, model, timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig), inlineCitations, }); - const payload = { + const payload = buildXaiWebSearchPayload({ query, provider: "grok", model, tookMs: Date.now() - start, - externalContent: { - untrusted: true, - source: "web_search", - provider: "grok", - wrapped: true, - }, - content: wrapWebContent(result.content), + content: result.content, citations: result.citations, inlineCitations: result.inlineCitations, - }; + }); writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig)); return payload; }, @@ -289,7 +156,15 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin { export const __testing = { resolveGrokApiKey, - resolveGrokModel, - resolveGrokInlineCitations, - extractGrokContent, + resolveGrokModel: (grok?: Record) => + resolveXaiWebSearchModel(grok ? { grok } : undefined), + resolveGrokInlineCitations: (grok?: Record) => + resolveXaiInlineCitations(grok ? { grok } : undefined), + extractGrokContent: extractXaiWebSearchContent, + extractXaiWebSearchContent, + resolveXaiInlineCitations, + resolveXaiSearchConfig, + resolveXaiWebSearchModel, + requestXaiWebSearch, + buildXaiWebSearchPayload, } as const; diff --git a/extensions/xai/src/web-search-shared.ts b/extensions/xai/src/web-search-shared.ts new file mode 100644 index 00000000000..47616bcf13c --- /dev/null +++ b/extensions/xai/src/web-search-shared.ts @@ -0,0 +1,171 @@ +import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; + +export const XAI_WEB_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses"; +export const XAI_DEFAULT_WEB_SEARCH_MODEL = "grok-4-1-fast"; + +export type XaiWebSearchResponse = { + output?: Array<{ + type?: string; + text?: string; + content?: Array<{ + type?: string; + text?: string; + annotations?: Array<{ + type?: string; + url?: string; + }>; + }>; + annotations?: Array<{ + type?: string; + url?: string; + }>; + }>; + output_text?: string; + citations?: string[]; + inline_citations?: Array<{ + start_index: number; + end_index: number; + url: string; + }>; +}; + +type XaiWebSearchConfig = Record & { + model?: unknown; + inlineCitations?: unknown; +}; + +export type XaiWebSearchResult = { + content: string; + citations: string[]; + inlineCitations?: XaiWebSearchResponse["inline_citations"]; +}; + +export function buildXaiWebSearchPayload(params: { + query: string; + provider: string; + model: string; + tookMs: number; + content: string; + citations: string[]; + inlineCitations?: XaiWebSearchResponse["inline_citations"]; +}): Record { + return { + query: params.query, + provider: params.provider, + model: params.model, + tookMs: params.tookMs, + externalContent: { + untrusted: true, + source: "web_search", + provider: params.provider, + wrapped: true, + }, + content: wrapWebContent(params.content, "web_search"), + citations: params.citations, + ...(params.inlineCitations ? { inlineCitations: params.inlineCitations } : {}), + }; +} + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +export function resolveXaiSearchConfig(searchConfig?: Record): XaiWebSearchConfig { + return (asRecord(searchConfig?.grok) as XaiWebSearchConfig | undefined) ?? {}; +} + +export function resolveXaiWebSearchModel(searchConfig?: Record): string { + const config = resolveXaiSearchConfig(searchConfig); + return typeof config.model === "string" && config.model.trim() + ? config.model.trim() + : XAI_DEFAULT_WEB_SEARCH_MODEL; +} + +export function resolveXaiInlineCitations(searchConfig?: Record): boolean { + return resolveXaiSearchConfig(searchConfig).inlineCitations === true; +} + +export function extractXaiWebSearchContent(data: XaiWebSearchResponse): { + text: string | undefined; + annotationCitations: string[]; +} { + for (const output of data.output ?? []) { + if (output.type === "message") { + for (const block of output.content ?? []) { + if (block.type === "output_text" && typeof block.text === "string" && block.text) { + const urls = (block.annotations ?? []) + .filter( + (annotation) => + annotation.type === "url_citation" && typeof annotation.url === "string", + ) + .map((annotation) => annotation.url as string); + return { text: block.text, annotationCitations: [...new Set(urls)] }; + } + } + } + + if (output.type === "output_text" && typeof output.text === "string" && output.text) { + const urls = (output.annotations ?? []) + .filter( + (annotation) => annotation.type === "url_citation" && typeof annotation.url === "string", + ) + .map((annotation) => annotation.url as string); + return { text: output.text, annotationCitations: [...new Set(urls)] }; + } + } + + return { + text: typeof data.output_text === "string" ? data.output_text : undefined, + annotationCitations: [], + }; +} + +export async function requestXaiWebSearch(params: { + query: string; + model: string; + apiKey: string; + timeoutSeconds: number; + inlineCitations: boolean; +}): Promise { + return await postTrustedWebToolsJson( + { + url: XAI_WEB_SEARCH_ENDPOINT, + timeoutSeconds: params.timeoutSeconds, + apiKey: params.apiKey, + body: { + model: params.model, + input: [{ role: "user", content: params.query }], + tools: [{ type: "web_search" }], + }, + errorLabel: "xAI", + }, + async (response) => { + const data = (await response.json()) as XaiWebSearchResponse; + const { text, annotationCitations } = extractXaiWebSearchContent(data); + const citations = + Array.isArray(data.citations) && data.citations.length > 0 + ? data.citations + : annotationCitations; + return { + content: text ?? "No response", + citations, + inlineCitations: + params.inlineCitations && Array.isArray(data.inline_citations) + ? data.inline_citations + : undefined, + }; + }, + ); +} + +export const __testing = { + buildXaiWebSearchPayload, + extractXaiWebSearchContent, + resolveXaiInlineCitations, + resolveXaiSearchConfig, + resolveXaiWebSearchModel, + requestXaiWebSearch, + XAI_DEFAULT_WEB_SEARCH_MODEL, +} as const; diff --git a/extensions/xai/web-search.ts b/extensions/xai/web-search.ts index c1d97652d54..d160892c0c5 100644 --- a/extensions/xai/web-search.ts +++ b/extensions/xai/web-search.ts @@ -5,133 +5,29 @@ import { getScopedCredentialValue, normalizeCacheKey, readCache, - readResponseText, + readNumberParam, + readStringParam, resolveCacheTtlMs, resolveTimeoutSeconds, resolveWebSearchProviderCredential, setScopedCredentialValue, type WebSearchProviderPlugin, - withTrustedWebToolsEndpoint, - wrapWebContent, writeCache, } from "openclaw/plugin-sdk/provider-web-search"; +import { + buildXaiWebSearchPayload, + extractXaiWebSearchContent, + requestXaiWebSearch, + resolveXaiInlineCitations, + resolveXaiWebSearchModel, +} from "./src/web-search-shared.js"; -const XAI_WEB_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses"; -const XAI_DEFAULT_WEB_SEARCH_MODEL = "grok-4-1-fast"; const XAI_WEB_SEARCH_CACHE = new Map< string, { value: Record; insertedAt: number; expiresAt: number } >(); -type XaiWebSearchResponse = { - output?: Array<{ - type?: string; - text?: string; - content?: Array<{ - type?: string; - text?: string; - annotations?: Array<{ - type?: string; - url?: string; - }>; - }>; - annotations?: Array<{ - type?: string; - url?: string; - }>; - }>; - output_text?: string; - citations?: string[]; - inline_citations?: Array<{ - start_index: number; - end_index: number; - url: string; - }>; -}; - -function extractXaiWebSearchContent(data: XaiWebSearchResponse): { - text: string | undefined; - annotationCitations: string[]; -} { - for (const output of data.output ?? []) { - if (output.type === "message") { - for (const block of output.content ?? []) { - if (block.type === "output_text" && typeof block.text === "string" && block.text) { - const urls = (block.annotations ?? []) - .filter( - (annotation) => - annotation.type === "url_citation" && typeof annotation.url === "string", - ) - .map((annotation) => annotation.url as string); - return { text: block.text, annotationCitations: [...new Set(urls)] }; - } - } - } - - if (output.type === "output_text" && typeof output.text === "string" && output.text) { - const urls = (output.annotations ?? []) - .filter( - (annotation) => annotation.type === "url_citation" && typeof annotation.url === "string", - ) - .map((annotation) => annotation.url as string); - return { text: output.text, annotationCitations: [...new Set(urls)] }; - } - } - - return { - text: typeof data.output_text === "string" ? data.output_text : undefined, - annotationCitations: [], - }; -} - -function asRecord(value: unknown): Record | undefined { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function resolveXaiWebSearchConfig( - searchConfig?: Record, -): Record { - return asRecord(searchConfig?.grok) ?? {}; -} - -function resolveXaiWebSearchModel(searchConfig?: Record): string { - const config = resolveXaiWebSearchConfig(searchConfig); - return typeof config.model === "string" && config.model.trim() - ? config.model.trim() - : XAI_DEFAULT_WEB_SEARCH_MODEL; -} - -function resolveXaiInlineCitations(searchConfig?: Record): boolean { - return resolveXaiWebSearchConfig(searchConfig).inlineCitations === true; -} - -function readQuery(args: Record): string { - const value = typeof args.query === "string" ? args.query.trim() : ""; - if (!value) { - throw new Error("query required"); - } - return value; -} - -function readCount(args: Record): number { - const raw = args.count; - const parsed = - typeof raw === "number" && Number.isFinite(raw) - ? raw - : typeof raw === "string" && raw.trim() - ? Number.parseFloat(raw) - : 5; - return Math.max(1, Math.min(10, Math.trunc(parsed))); -} - -async function throwXaiWebSearchApiError(res: Response): Promise { - const detailResult = await readResponseText(res, { maxBytes: 64_000 }); - throw new Error(`xAI API error (${res.status}): ${detailResult.text || res.statusText}`); -} - -async function runXaiWebSearch(params: { +function runXaiWebSearch(params: { query: string; model: string; apiKey: string; @@ -144,61 +40,31 @@ async function runXaiWebSearch(params: { ); const cached = readCache(XAI_WEB_SEARCH_CACHE, cacheKey); if (cached) { - return { ...cached.value, cached: true }; + return Promise.resolve({ ...cached.value, cached: true }); } - const startedAt = Date.now(); - const payload = await withTrustedWebToolsEndpoint( - { - url: XAI_WEB_SEARCH_ENDPOINT, + return (async () => { + const startedAt = Date.now(); + const result = await requestXaiWebSearch({ + query: params.query, + model: params.model, + apiKey: params.apiKey, timeoutSeconds: params.timeoutSeconds, - init: { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${params.apiKey}`, - }, - body: JSON.stringify({ - model: params.model, - input: [{ role: "user", content: params.query }], - tools: [{ type: "web_search" }], - }), - }, - }, - async ({ response }) => { - if (!response.ok) { - return await throwXaiWebSearchApiError(response); - } + inlineCitations: params.inlineCitations, + }); + const payload = buildXaiWebSearchPayload({ + query: params.query, + provider: "grok", + model: params.model, + tookMs: Date.now() - startedAt, + content: result.content, + citations: result.citations, + inlineCitations: result.inlineCitations, + }); - const data = (await response.json()) as XaiWebSearchResponse; - const { text, annotationCitations } = extractXaiWebSearchContent(data); - const citations = - Array.isArray(data.citations) && data.citations.length > 0 - ? data.citations - : annotationCitations; - - return { - query: params.query, - provider: "grok", - model: params.model, - tookMs: Date.now() - startedAt, - externalContent: { - untrusted: true, - source: "web_search", - provider: "grok", - wrapped: true, - }, - content: wrapWebContent(text ?? "No response", "web_search"), - citations, - ...(params.inlineCitations && Array.isArray(data.inline_citations) - ? { inlineCitations: data.inline_citations } - : {}), - }; - }, - ); - - writeCache(XAI_WEB_SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs); - return payload; + writeCache(XAI_WEB_SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs); + return payload; + })(); } export function createXaiWebSearchProvider(): WebSearchProviderPlugin { @@ -246,8 +112,9 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin { }; } - const query = readQuery(args); - const count = readCount(args); + const query = readStringParam(args, "query", { required: true }); + void readNumberParam(args, "count", { integer: true }); + return await runXaiWebSearch({ query, model: resolveXaiWebSearchModel(ctx.searchConfig), @@ -268,7 +135,9 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin { } export const __testing = { + buildXaiWebSearchPayload, extractXaiWebSearchContent, - resolveXaiWebSearchModel, resolveXaiInlineCitations, + resolveXaiWebSearchModel, + requestXaiWebSearch, }; diff --git a/extensions/zai/runtime-api.ts b/extensions/zai/runtime-api.ts index 16d46dd4362..f512627cde8 100644 --- a/extensions/zai/runtime-api.ts +++ b/extensions/zai/runtime-api.ts @@ -1 +1,5 @@ -export * from "../../src/plugin-sdk/zai.js"; +export { + detectZaiEndpoint, + type ZaiDetectedEndpoint, + type ZaiEndpointId, +} from "openclaw/plugin-sdk/provider-zai-endpoint"; diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 24e46323a8d..571ad31c164 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -39,7 +39,12 @@ import { probeZalouser } from "./probe.js"; import { writeQrDataUrlToTempFile } from "./qr-temp-file.js"; import { getZalouserRuntime } from "./runtime.js"; import { sendMessageZalouser, sendReactionZalouser } from "./send.js"; -import { resolveZalouserOutboundSessionRoute } from "./session-route.js"; +import { + normalizeZalouserTarget, + parseZalouserDirectoryGroupId, + parseZalouserOutboundTarget, + resolveZalouserOutboundSessionRoute, +} from "./session-route.js"; import { zalouserSetupAdapter } from "./setup-core.js"; import { zalouserSetupWizard } from "./setup-surface.js"; import { createZalouserPluginBase } from "./shared.js"; @@ -56,97 +61,6 @@ import { const ZALOUSER_TEXT_CHUNK_LIMIT = 2000; -function stripZalouserTargetPrefix(raw: string): string { - return raw - .trim() - .replace(/^(zalouser|zlu):/i, "") - .trim(); -} - -function normalizePrefixedTarget(raw: string): string | undefined { - const trimmed = stripZalouserTargetPrefix(raw); - if (!trimmed) { - return undefined; - } - - const lower = trimmed.toLowerCase(); - if (lower.startsWith("group:")) { - const id = trimmed.slice("group:".length).trim(); - return id ? `group:${id}` : undefined; - } - if (lower.startsWith("g:")) { - const id = trimmed.slice("g:".length).trim(); - return id ? `group:${id}` : undefined; - } - if (lower.startsWith("user:")) { - const id = trimmed.slice("user:".length).trim(); - return id ? `user:${id}` : undefined; - } - if (lower.startsWith("dm:")) { - const id = trimmed.slice("dm:".length).trim(); - return id ? `user:${id}` : undefined; - } - if (lower.startsWith("u:")) { - const id = trimmed.slice("u:".length).trim(); - return id ? `user:${id}` : undefined; - } - if (/^g-\S+$/i.test(trimmed)) { - return `group:${trimmed}`; - } - if (/^u-\S+$/i.test(trimmed)) { - return `user:${trimmed}`; - } - - return trimmed; -} - -function parseZalouserOutboundTarget(raw: string): { - threadId: string; - isGroup: boolean; -} { - const normalized = normalizePrefixedTarget(raw); - if (!normalized) { - throw new Error("Zalouser target is required"); - } - const lowered = normalized.toLowerCase(); - if (lowered.startsWith("group:")) { - const threadId = normalized.slice("group:".length).trim(); - if (!threadId) { - throw new Error("Zalouser group target is missing group id"); - } - return { threadId, isGroup: true }; - } - if (lowered.startsWith("user:")) { - const threadId = normalized.slice("user:".length).trim(); - if (!threadId) { - throw new Error("Zalouser user target is missing user id"); - } - return { threadId, isGroup: false }; - } - // Backward-compatible fallback for bare IDs. - // Group sends should use explicit `group:` targets. - return { threadId: normalized, isGroup: false }; -} - -function parseZalouserDirectoryGroupId(raw: string): string { - const normalized = normalizePrefixedTarget(raw); - if (!normalized) { - throw new Error("Zalouser group target is required"); - } - const lowered = normalized.toLowerCase(); - if (lowered.startsWith("group:")) { - const groupId = normalized.slice("group:".length).trim(); - if (!groupId) { - throw new Error("Zalouser group target is missing group id"); - } - return groupId; - } - if (lowered.startsWith("user:")) { - throw new Error("Zalouser group members lookup requires a group target (group:)"); - } - return normalized; -} - function resolveZalouserQrProfile(accountId?: string | null): string { const normalized = normalizeAccountId(accountId); if (!normalized || normalized === DEFAULT_ACCOUNT_ID) { @@ -318,11 +232,11 @@ export const zalouserPlugin: ChannelPlugin = { }, actions: zalouserMessageActions, messaging: { - normalizeTarget: (raw) => normalizePrefixedTarget(raw), + normalizeTarget: (raw) => normalizeZalouserTarget(raw), resolveOutboundSessionRoute: (params) => resolveZalouserOutboundSessionRoute(params), targetResolver: { looksLikeId: (raw) => { - const normalized = normalizePrefixedTarget(raw); + const normalized = normalizeZalouserTarget(raw); if (!normalized) { return false; } diff --git a/extensions/zalouser/src/session-route.ts b/extensions/zalouser/src/session-route.ts index c6a1761818d..1356ec434c0 100644 --- a/extensions/zalouser/src/session-route.ts +++ b/extensions/zalouser/src/session-route.ts @@ -3,14 +3,14 @@ import { type ChannelOutboundSessionRouteParams, } from "openclaw/plugin-sdk/core"; -function stripZalouserTargetPrefix(raw: string): string { +export function stripZalouserTargetPrefix(raw: string): string { return raw .trim() .replace(/^(zalouser|zlu):/i, "") .trim(); } -function normalizePrefixedTarget(raw: string): string | undefined { +export function normalizeZalouserTarget(raw: string): string | undefined { const trimmed = stripZalouserTargetPrefix(raw); if (!trimmed) { return undefined; @@ -47,8 +47,55 @@ function normalizePrefixedTarget(raw: string): string | undefined { return trimmed; } +export function parseZalouserOutboundTarget(raw: string): { + threadId: string; + isGroup: boolean; +} { + const normalized = normalizeZalouserTarget(raw); + if (!normalized) { + throw new Error("Zalouser target is required"); + } + const lowered = normalized.toLowerCase(); + if (lowered.startsWith("group:")) { + const threadId = normalized.slice("group:".length).trim(); + if (!threadId) { + throw new Error("Zalouser group target is missing group id"); + } + return { threadId, isGroup: true }; + } + if (lowered.startsWith("user:")) { + const threadId = normalized.slice("user:".length).trim(); + if (!threadId) { + throw new Error("Zalouser user target is missing user id"); + } + return { threadId, isGroup: false }; + } + // Backward-compatible fallback for bare IDs. + // Group sends should use explicit `group:` targets. + return { threadId: normalized, isGroup: false }; +} + +export function parseZalouserDirectoryGroupId(raw: string): string { + const normalized = normalizeZalouserTarget(raw); + if (!normalized) { + throw new Error("Zalouser group target is required"); + } + const lowered = normalized.toLowerCase(); + if (lowered.startsWith("group:")) { + const groupId = normalized.slice("group:".length).trim(); + if (!groupId) { + throw new Error("Zalouser group target is missing group id"); + } + return groupId; + } + if (lowered.startsWith("user:")) { + throw new Error("Zalouser group members lookup requires a group target (group:)"); + } + return normalized; +} + export function resolveZalouserOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { - const normalized = normalizePrefixedTarget(params.target); + const normalized = normalizeZalouserTarget(params.target); if (!normalized) { return null; } diff --git a/package.json b/package.json index b8fe827b3e7..a522ced8380 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,10 @@ "types": "./dist/plugin-sdk/process-runtime.d.ts", "default": "./dist/plugin-sdk/process-runtime.js" }, + "./plugin-sdk/windows-spawn": { + "types": "./dist/plugin-sdk/windows-spawn.d.ts", + "default": "./dist/plugin-sdk/windows-spawn.js" + }, "./plugin-sdk/acp-runtime": { "types": "./dist/plugin-sdk/acp-runtime.d.ts", "default": "./dist/plugin-sdk/acp-runtime.js" @@ -357,6 +361,14 @@ "types": "./dist/plugin-sdk/provider-catalog.d.ts", "default": "./dist/plugin-sdk/provider-catalog.js" }, + "./plugin-sdk/provider-env-vars": { + "types": "./dist/plugin-sdk/provider-env-vars.d.ts", + "default": "./dist/plugin-sdk/provider-env-vars.js" + }, + "./plugin-sdk/provider-google": { + "types": "./dist/plugin-sdk/provider-google.d.ts", + "default": "./dist/plugin-sdk/provider-google.js" + }, "./plugin-sdk/provider-models": { "types": "./dist/plugin-sdk/provider-models.d.ts", "default": "./dist/plugin-sdk/provider-models.js" @@ -377,6 +389,10 @@ "types": "./dist/plugin-sdk/provider-web-search.d.ts", "default": "./dist/plugin-sdk/provider-web-search.js" }, + "./plugin-sdk/provider-zai-endpoint": { + "types": "./dist/plugin-sdk/provider-zai-endpoint.d.ts", + "default": "./dist/plugin-sdk/provider-zai-endpoint.js" + }, "./plugin-sdk/image-generation": { "types": "./dist/plugin-sdk/image-generation.d.ts", "default": "./dist/plugin-sdk/image-generation.js" diff --git a/scripts/check-plugin-extension-import-boundary.mjs b/scripts/check-plugin-extension-import-boundary.mjs index 13c4fa596a3..bbe9f9702f5 100644 --- a/scripts/check-plugin-extension-import-boundary.mjs +++ b/scripts/check-plugin-extension-import-boundary.mjs @@ -194,7 +194,10 @@ function scanWebSearchRegistrySmells(sourceFile, filePath) { function shouldSkipFile(filePath) { const relativeFile = normalizePath(filePath); - return relativeFile.startsWith("src/plugins/contracts/"); + return ( + relativeFile.startsWith("src/plugins/contracts/") || + /^src\/plugins\/runtime\/runtime-[^/]+-contract\.[cm]?[jt]s$/u.test(relativeFile) + ); } export async function collectPluginExtensionImportBoundaryInventory() { diff --git a/scripts/check-plugin-sdk-exports.mjs b/scripts/check-plugin-sdk-exports.mjs index 60c89056ca0..90d784235f5 100755 --- a/scripts/check-plugin-sdk-exports.mjs +++ b/scripts/check-plugin-sdk-exports.mjs @@ -42,7 +42,7 @@ const exportedNames = exportMatch[1] const exportSet = new Set(exportedNames); -const requiredRuntimeShimEntries = ["root-alias.cjs"]; +const requiredRuntimeShimEntries = ["compat.js", "root-alias.cjs"]; // Critical functions that channel extension plugins import from openclaw/plugin-sdk. // If any of these are missing, plugins will fail at runtime with: @@ -65,6 +65,7 @@ const requiredExports = [ "resolveChannelMediaMaxBytes", "warnMissingProviderGroupPolicyFallbackOnce", "emptyPluginConfigSchema", + "onDiagnosticEvent", "normalizePluginHttpPath", "registerPluginHttpRoute", "DEFAULT_ACCOUNT_ID", diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index e1991f4ab76..e5dad4777eb 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -32,6 +32,7 @@ "cli-runtime", "hook-runtime", "process-runtime", + "windows-spawn", "acp-runtime", "telegram", "telegram-core", @@ -79,11 +80,14 @@ "provider-auth-login", "plugin-entry", "provider-catalog", + "provider-env-vars", + "provider-google", "provider-models", "provider-onboard", "provider-stream", "provider-usage", "provider-web-search", + "provider-zai-endpoint", "image-generation", "reply-history", "media-understanding", diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 72d729cc1cd..f7f36373a49 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -21,6 +21,7 @@ const requiredPathGroups = [ ["dist/index.js", "dist/index.mjs"], ["dist/entry.js", "dist/entry.mjs"], ...listPluginSdkDistArtifacts(), + "dist/plugin-sdk/compat.js", "dist/plugin-sdk/root-alias.cjs", "dist/build-info.json", ]; @@ -228,6 +229,7 @@ const requiredPluginSdkExports = [ "resolveChannelMediaMaxBytes", "warnMissingProviderGroupPolicyFallbackOnce", "emptyPluginConfigSchema", + "onDiagnosticEvent", "normalizePluginHttpPath", "registerPluginHttpRoute", "DEFAULT_ACCOUNT_ID", diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 8fc8ac1fddc..35fc741db58 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -4,7 +4,9 @@ import { BILLING_ERROR_USER_MESSAGE, formatBillingErrorMessage, formatAssistantErrorText, + getApiErrorPayloadFingerprint, formatRawAssistantErrorForUi, + isRawApiErrorPayload, } from "./pi-embedded-helpers.js"; import { makeAssistantMessageFixture } from "./test-helpers/assistant-message-fixtures.js"; @@ -159,3 +161,14 @@ describe("formatRawAssistantErrorForUi", () => { ); }); }); + +describe("raw API error payload helpers", () => { + it("recognizes provider-prefixed JSON payloads for observation fingerprints", () => { + const raw = + 'Ollama API error: {"type":"error","error":{"type":"server_error","message":"Boom"},"request_id":"req_123"}'; + + expect(isRawApiErrorPayload(raw)).toBe(true); + expect(getApiErrorPayloadFingerprint(raw)).toContain("server_error"); + expect(getApiErrorPayloadFingerprint(raw)).toContain("req_123"); + }); +}); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 2fec27a45e2..7719ecb41a0 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -5,6 +5,7 @@ import { extractLeadingHttpStatus, formatRawAssistantErrorForUi, isCloudflareOrHtmlErrorPage, + parseApiErrorPayload, } from "../../shared/assistant-error-format.js"; export { extractLeadingHttpStatus, @@ -223,9 +224,6 @@ export function extractObservedOverflowTokenCount(errorMessage?: string): number return undefined; } -// Allow provider-wrapped API payloads such as "Ollama API error 400: {...}". -const ERROR_PAYLOAD_PREFIX_RE = - /^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error)(?:\s+\d{3})?[:\s-]+/i; const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/gi; const ERROR_PREFIX_RE = /^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|openai\s*error|anthropic\s*error|gateway\s*error|request failed|failed|exception)(?:\s+\d{3})?[:\s-]+/i; @@ -482,63 +480,6 @@ function shouldRewriteContextOverflowText(raw: string): boolean { ); } -type ErrorPayload = Record; - -function isErrorPayloadObject(payload: unknown): payload is ErrorPayload { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) { - return false; - } - const record = payload as ErrorPayload; - if (record.type === "error") { - return true; - } - if (typeof record.request_id === "string" || typeof record.requestId === "string") { - return true; - } - if ("error" in record) { - const err = record.error; - if (err && typeof err === "object" && !Array.isArray(err)) { - const errRecord = err as ErrorPayload; - if ( - typeof errRecord.message === "string" || - typeof errRecord.type === "string" || - typeof errRecord.code === "string" - ) { - return true; - } - } - } - return false; -} - -function parseApiErrorPayload(raw: string): ErrorPayload | null { - if (!raw) { - return null; - } - const trimmed = raw.trim(); - if (!trimmed) { - return null; - } - const candidates = [trimmed]; - if (ERROR_PAYLOAD_PREFIX_RE.test(trimmed)) { - candidates.push(trimmed.replace(ERROR_PAYLOAD_PREFIX_RE, "").trim()); - } - for (const candidate of candidates) { - if (!candidate.startsWith("{") || !candidate.endsWith("}")) { - continue; - } - try { - const parsed = JSON.parse(candidate) as unknown; - if (isErrorPayloadObject(parsed)) { - return parsed; - } - } catch { - // ignore parse errors - } - } - return null; -} - export function getApiErrorPayloadFingerprint(raw?: string): string | null { if (!raw) { return null; diff --git a/src/agents/tools/web-search-provider-common.ts b/src/agents/tools/web-search-provider-common.ts index f69876ed04a..79827ef7cb8 100644 --- a/src/agents/tools/web-search-provider-common.ts +++ b/src/agents/tools/web-search-provider-common.ts @@ -92,6 +92,45 @@ export async function withTrustedWebSearchEndpoint( ); } +export async function postTrustedWebToolsJson( + params: { + url: string; + timeoutSeconds: number; + apiKey: string; + body: Record; + errorLabel: string; + maxErrorBytes?: number; + }, + parseResponse: (response: Response) => Promise, +): Promise { + return withTrustedWebToolsEndpoint( + { + url: params.url, + timeoutSeconds: params.timeoutSeconds, + init: { + method: "POST", + headers: { + Accept: "application/json", + Authorization: `Bearer ${params.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(params.body), + }, + }, + async ({ response }) => { + if (!response.ok) { + const detail = await readResponseText(response, { + maxBytes: params.maxErrorBytes ?? 64_000, + }); + throw new Error( + `${params.errorLabel} API error (${response.status}): ${detail.text || response.statusText}`, + ); + } + return await parseResponse(response); + }, + ); +} + export async function throwWebSearchApiError(res: Response, providerLabel: string): Promise { const detailResult = await readResponseText(res, { maxBytes: 64_000 }); const detail = detailResult.text; diff --git a/src/channels/thread-bindings-policy.ts b/src/channels/thread-bindings-policy.ts index 5fe30994da0..730984d61df 100644 --- a/src/channels/thread-bindings-policy.ts +++ b/src/channels/thread-bindings-policy.ts @@ -73,6 +73,58 @@ export function resolveThreadBindingMaxAgeMs(params: { return Math.floor(maxAgeHours * 60 * 60 * 1000); } +type ThreadBindingLifecycleRecord = { + boundAt: number; + lastActivityAt: number; + idleTimeoutMs?: number; + maxAgeMs?: number; +}; + +export function resolveThreadBindingLifecycle(params: { + record: ThreadBindingLifecycleRecord; + defaultIdleTimeoutMs: number; + defaultMaxAgeMs: number; +}): { + expiresAt?: number; + reason?: "idle-expired" | "max-age-expired"; +} { + const idleTimeoutMs = + typeof params.record.idleTimeoutMs === "number" + ? Math.max(0, Math.floor(params.record.idleTimeoutMs)) + : params.defaultIdleTimeoutMs; + const maxAgeMs = + typeof params.record.maxAgeMs === "number" + ? Math.max(0, Math.floor(params.record.maxAgeMs)) + : params.defaultMaxAgeMs; + + const inactivityExpiresAt = + idleTimeoutMs > 0 + ? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs + : undefined; + const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined; + + if (inactivityExpiresAt != null && maxAgeExpiresAt != null) { + return inactivityExpiresAt <= maxAgeExpiresAt + ? { expiresAt: inactivityExpiresAt, reason: "idle-expired" } + : { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" }; + } + if (inactivityExpiresAt != null) { + return { expiresAt: inactivityExpiresAt, reason: "idle-expired" }; + } + if (maxAgeExpiresAt != null) { + return { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" }; + } + return {}; +} + +export function resolveThreadBindingEffectiveExpiresAt(params: { + record: ThreadBindingLifecycleRecord; + defaultIdleTimeoutMs: number; + defaultMaxAgeMs: number; +}): number | undefined { + return resolveThreadBindingLifecycle(params).expiresAt; +} + export function resolveThreadBindingsEnabled(params: { channelEnabledRaw: unknown; sessionEnabledRaw: unknown; diff --git a/src/infra/tsdown-config.test.ts b/src/infra/tsdown-config.test.ts index 94332c5b307..c47bbcb2192 100644 --- a/src/infra/tsdown-config.test.ts +++ b/src/infra/tsdown-config.test.ts @@ -36,6 +36,7 @@ describe("tsdown config", () => { expect.arrayContaining([ "index", "plugins/runtime/index", + "plugin-sdk/compat", "plugin-sdk/index", "extensions/openai/index", "bundled/boot-md/handler", diff --git a/src/plugin-sdk/acp-runtime.ts b/src/plugin-sdk/acp-runtime.ts index 84435bb896a..7767d042f9b 100644 --- a/src/plugin-sdk/acp-runtime.ts +++ b/src/plugin-sdk/acp-runtime.ts @@ -2,6 +2,7 @@ export { getAcpSessionManager } from "../acp/control-plane/manager.js"; export { AcpRuntimeError, isAcpRuntimeError } from "../acp/runtime/errors.js"; +export { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../acp/runtime/registry.js"; export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js"; export type { AcpRuntime, diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index 5e2bcd11f58..99e2066633c 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -20,6 +20,8 @@ if (shouldWarnCompatImport) { export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; export { resolveControlCommandGate } from "../channels/command-gating.js"; export { delegateCompactionToRuntime } from "../context-engine/delegate.js"; +export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js"; +export { onDiagnosticEvent } from "../infra/diagnostic-events.js"; export { createAccountStatusSink } from "./channel-lifecycle.js"; export { createPluginRuntimeStore } from "./runtime-store.js"; diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 38509cef4ab..c8c7980fbd2 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -51,6 +51,8 @@ export type { ProviderAuthMethodNonInteractiveContext, ProviderAuthMethod, ProviderAuthResult, + OpenClawPluginToolContext, + OpenClawPluginToolFactory, OpenClawPluginCommandDefinition, OpenClawPluginDefinition, PluginCommandContext, diff --git a/src/plugin-sdk/index.test.ts b/src/plugin-sdk/index.test.ts index 30040416729..db54ebbd1ff 100644 --- a/src/plugin-sdk/index.test.ts +++ b/src/plugin-sdk/index.test.ts @@ -50,9 +50,11 @@ describe("plugin-sdk exports", () => { it("keeps the root runtime surface intentionally small", () => { expect(typeof sdk.emptyPluginConfigSchema).toBe("function"); expect(typeof sdk.delegateCompactionToRuntime).toBe("function"); + expect(typeof sdk.onDiagnosticEvent).toBe("function"); expect(Object.prototype.hasOwnProperty.call(sdk, "resolveControlCommandGate")).toBe(false); expect(Object.prototype.hasOwnProperty.call(sdk, "buildAgentSessionKey")).toBe(false); expect(Object.prototype.hasOwnProperty.call(sdk, "isDangerousNameMatchingEnabled")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(sdk, "emitDiagnosticEvent")).toBe(false); }); it("keeps package.json plugin-sdk exports synced with the manifest", async () => { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 5bb67920734..20f8a34672a 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -64,7 +64,9 @@ export type { HookEntry } from "../hooks/types.js"; export type { ReplyPayload } from "../auto-reply/types.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export type { ContextEngineFactory } from "../context-engine/registry.js"; +export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; export { registerContextEngine } from "../context-engine/registry.js"; export { delegateCompactionToRuntime } from "../context-engine/delegate.js"; +export { onDiagnosticEvent } from "../infra/diagnostic-events.js"; diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts index 660fe7183fb..3d6ff402d59 100644 --- a/src/plugin-sdk/matrix.ts +++ b/src/plugin-sdk/matrix.ts @@ -1,178 +1 @@ -// Narrow plugin-sdk surface for the bundled matrix plugin. -// Keep this list additive and scoped to symbols used under extensions/matrix. - -import { createOptionalChannelSetupSurface } from "./channel-setup.js"; - -export { - createActionGate, - jsonResult, - readNumberParam, - readReactionParams, - readStringArrayParam, - readStringParam, -} from "../agents/tools/common.js"; -export type { ReplyPayload } from "../auto-reply/types.js"; -export { resolveAckReaction } from "../agents/identity.js"; -export { - compileAllowlist, - resolveCompiledAllowlistMatch, - resolveAllowlistCandidates, - resolveAllowlistMatchByCandidates, -} from "../channels/allowlist-match.js"; -export { - addAllowlistUserEntriesFromConfigEntry, - buildAllowlistResolutionSummary, - canonicalizeAllowlistWithResolvedIds, - mergeAllowlist, - patchAllowlistUsersInConfigEntries, - summarizeMapping, -} from "../channels/allowlists/resolve-utils.js"; -export { ensureConfiguredAcpBindingReady } from "../acp/persistent-bindings.lifecycle.js"; -export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; -export { resolveControlCommandGate } from "../channels/command-gating.js"; -export type { NormalizedLocation } from "../channels/location.js"; -export { formatLocationText, toLocationContext } from "../channels/location.js"; -export { logInboundDrop, logTypingFailure } from "../channels/logging.js"; -export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; -export { formatAllowlistMatchMeta } from "../channels/plugins/allowlist-match.js"; -export { - buildChannelKeyCandidates, - resolveChannelEntryMatch, -} from "../channels/plugins/channel-config.js"; -export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; -export { - deleteAccountFromConfigSection, - setAccountEnabledInConfigSection, -} from "../channels/plugins/config-helpers.js"; -export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; -export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; -export { - buildSingleChannelSecretPromptState, - addWildcardAllowFrom, - mergeAllowFromEntries, - promptAccountId, - promptSingleChannelSecretInput, - setTopLevelChannelGroupPolicy, -} from "../channels/plugins/setup-wizard-helpers.js"; -export { promptChannelAccessConfig } from "../channels/plugins/setup-group-access.js"; -export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; -export { - applyAccountNameToChannelSection, - moveSingleAccountChannelSectionToDefaultAccount, -} from "../channels/plugins/setup-helpers.js"; -export type { - BaseProbeResult, - ChannelDirectoryEntry, - ChannelGroupContext, - ChannelMessageActionAdapter, - ChannelMessageActionContext, - ChannelMessageActionName, - ChannelMessageToolDiscovery, - ChannelMessageToolSchemaContribution, - ChannelOutboundAdapter, - ChannelResolveKind, - ChannelResolveResult, - ChannelSetupInput, - ChannelToolSend, -} from "../channels/plugins/types.js"; -export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; -export { resolveThreadBindingFarewellText } from "../channels/thread-bindings-messages.js"; -export { - resolveThreadBindingIdleTimeoutMsForChannel, - resolveThreadBindingMaxAgeMsForChannel, -} from "../channels/thread-bindings-policy.js"; -export { - setMatrixThreadBindingIdleTimeoutBySessionKey, - setMatrixThreadBindingMaxAgeBySessionKey, -} from "../../extensions/matrix/thread-bindings-runtime.js"; -export { createTypingCallbacks } from "../channels/typing.js"; -export { createChannelReplyPipeline } from "./channel-reply-pipeline.js"; -export type { OpenClawConfig } from "../config/config.js"; -export { - GROUP_POLICY_BLOCKED_LABEL, - resolveAllowlistProviderRuntimeGroupPolicy, - resolveDefaultGroupPolicy, - warnMissingProviderGroupPolicyFallbackOnce, -} from "../config/runtime-group-policy.js"; -export type { - DmPolicy, - GroupPolicy, - GroupToolPolicyConfig, - MarkdownTableMode, -} from "../config/types.js"; -export type { SecretInput } from "./secret-input.js"; -export { - buildSecretInputSchema, - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "./secret-input.js"; -export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; -export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; -export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js"; -export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; -export { maybeCreateMatrixMigrationSnapshot } from "../infra/matrix-migration-snapshot.js"; -export { - getSessionBindingService, - registerSessionBindingAdapter, - unregisterSessionBindingAdapter, -} from "../infra/outbound/session-binding-service.js"; -export { resolveOutboundSendDep } from "../infra/outbound/send-deps.js"; -export type { - BindingTargetKind, - SessionBindingRecord, -} from "../infra/outbound/session-binding-service.js"; -export { isPrivateOrLoopbackHost } from "../gateway/net.js"; -export { getAgentScopedMediaLocalRoots } from "../media/local-roots.js"; -export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; -export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js"; -export type { OpenClawPluginApi } from "../plugins/types.js"; -export type { PollInput } from "../polls.js"; -export { normalizePollInput } from "../polls.js"; -export { - DEFAULT_ACCOUNT_ID, - normalizeAccountId, - normalizeOptionalAccountId, - resolveAgentIdFromSessionKey, -} from "../routing/session-key.js"; -export type { RuntimeEnv } from "../runtime.js"; -export { normalizeStringEntries } from "../shared/string-normalization.js"; -export { formatDocsLink } from "../terminal/links.js"; -export { redactSensitiveText } from "../logging/redact.js"; -export type { WizardPrompter } from "../wizard/prompts.js"; -export { - evaluateGroupRouteAccessForPolicy, - resolveSenderScopedGroupPolicy, -} from "./group-access.js"; -export { createChannelPairingController } from "./channel-pairing.js"; -export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js"; -export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; -export { runPluginCommandWithTimeout } from "./run-command.js"; -export { createLoggerBackedRuntime, resolveRuntimeEnv } from "./runtime.js"; -export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; -export { - buildProbeChannelStatusSummary, - collectStatusIssuesFromLastError, -} from "./status-helpers.js"; -export { - resolveMatrixAccountStorageRoot, - resolveMatrixCredentialsDir, - resolveMatrixCredentialsPath, - resolveMatrixLegacyFlatStoragePaths, -} from "../../extensions/matrix/helper-api.js"; -export { getMatrixScopedEnvVarNames } from "../../extensions/matrix/helper-api.js"; -export { - requiresExplicitMatrixDefaultAccount, - resolveMatrixDefaultOrOnlyAccountId, -} from "../../extensions/matrix/helper-api.js"; - -const matrixSetup = createOptionalChannelSetupSurface({ - channel: "matrix", - label: "Matrix", - npmSpec: "@openclaw/matrix", - docsPath: "/channels/matrix", -}); - -export const matrixSetupWizard = matrixSetup.setupWizard; -export const matrixSetupAdapter = matrixSetup.setupAdapter; +export * from "../plugins/runtime/runtime-matrix-contract.js"; diff --git a/src/plugin-sdk/provider-env-vars.ts b/src/plugin-sdk/provider-env-vars.ts new file mode 100644 index 00000000000..fb4d0271bf1 --- /dev/null +++ b/src/plugin-sdk/provider-env-vars.ts @@ -0,0 +1,6 @@ +// Public provider auth environment variable helpers for plugin runtimes. + +export { + listKnownProviderAuthEnvVarNames, + omitEnvKeysCaseInsensitive, +} from "../secrets/provider-env-vars.js"; diff --git a/src/plugin-sdk/provider-google.ts b/src/plugin-sdk/provider-google.ts new file mode 100644 index 00000000000..43130b853ca --- /dev/null +++ b/src/plugin-sdk/provider-google.ts @@ -0,0 +1,4 @@ +// Public Google provider helpers shared by bundled Google extensions. + +export { normalizeGoogleModelId } from "../agents/model-id-normalization.js"; +export { parseGeminiAuth } from "../infra/gemini-auth.js"; diff --git a/src/plugin-sdk/provider-web-search.ts b/src/plugin-sdk/provider-web-search.ts index 258d26e7ee4..9ed067cbf23 100644 --- a/src/plugin-sdk/provider-web-search.ts +++ b/src/plugin-sdk/provider-web-search.ts @@ -23,6 +23,7 @@ export { resolveSearchCount, resolveSearchTimeoutSeconds, resolveSiteName, + postTrustedWebToolsJson, throwWebSearchApiError, withTrustedWebSearchEndpoint, writeCachedSearchPayload, diff --git a/src/plugin-sdk/provider-zai-endpoint.ts b/src/plugin-sdk/provider-zai-endpoint.ts new file mode 100644 index 00000000000..d2c288b7ed6 --- /dev/null +++ b/src/plugin-sdk/provider-zai-endpoint.ts @@ -0,0 +1,7 @@ +// Public Z.AI endpoint detection helpers for provider plugins. + +export { + detectZaiEndpoint, + type ZaiDetectedEndpoint, + type ZaiEndpointId, +} from "../plugins/provider-zai-endpoint.js"; diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 23e583f8c4d..669586bb80c 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -5,6 +5,7 @@ const fs = require("node:fs"); let monolithicSdk = null; const jitiLoaders = new Map(); +const pluginSdkSubpathsCache = new Map(); function emptyPluginConfigSchema() { function error(message) { @@ -61,6 +62,49 @@ function resolveControlCommandGate(params) { return { commandAuthorized, shouldBlock }; } +function getPackageRoot() { + return path.resolve(__dirname, "..", ".."); +} + +function listPluginSdkExportedSubpaths() { + const packageRoot = getPackageRoot(); + if (pluginSdkSubpathsCache.has(packageRoot)) { + return pluginSdkSubpathsCache.get(packageRoot); + } + + let subpaths = []; + try { + const packageJsonPath = path.join(packageRoot, "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + subpaths = Object.keys(packageJson.exports ?? {}) + .filter((key) => key.startsWith("./plugin-sdk/")) + .map((key) => key.slice("./plugin-sdk/".length)); + } catch { + subpaths = []; + } + + pluginSdkSubpathsCache.set(packageRoot, subpaths); + return subpaths; +} + +function buildPluginSdkAliasMap(useDist) { + const packageRoot = getPackageRoot(); + const pluginSdkDir = path.join(packageRoot, useDist ? "dist" : "src", "plugin-sdk"); + const ext = useDist ? ".js" : ".ts"; + const aliasMap = { + "openclaw/plugin-sdk": __filename, + }; + + for (const subpath of listPluginSdkExportedSubpaths()) { + const candidate = path.join(pluginSdkDir, `${subpath}${ext}`); + if (fs.existsSync(candidate)) { + aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate; + } + } + + return aliasMap; +} + function getJiti(tryNative) { if (jitiLoaders.has(tryNative)) { return jitiLoaders.get(tryNative); @@ -68,6 +112,7 @@ function getJiti(tryNative) { const { createJiti } = require("jiti"); const jitiLoader = createJiti(__filename, { + alias: buildPluginSdkAliasMap(tryNative), interopDefault: true, // Prefer Node's native sync ESM loader for built dist/plugin-sdk/*.js files // so local plugins do not create a second transpiled OpenClaw core graph. diff --git a/src/plugin-sdk/root-alias.test.ts b/src/plugin-sdk/root-alias.test.ts index 83937c34b44..48ae4a7b43c 100644 --- a/src/plugin-sdk/root-alias.test.ts +++ b/src/plugin-sdk/root-alias.test.ts @@ -48,6 +48,12 @@ function loadRootAliasWithStubs(options?: { } if (id === "node:fs") { return { + readFileSync: () => + JSON.stringify({ + exports: { + "./plugin-sdk/group-access": { default: "./dist/plugin-sdk/group-access.js" }, + }, + }), existsSync: () => options?.distExists ?? false, }; } @@ -164,8 +170,23 @@ describe("plugin-sdk root alias", () => { expect("delegateCompactionToRuntime" in lazyRootSdk).toBe(true); }); + it("forwards onDiagnosticEvent through the compat-backed root alias", () => { + const onDiagnosticEvent = () => () => undefined; + const lazyModule = loadRootAliasWithStubs({ + monolithicExports: { + onDiagnosticEvent, + }, + }); + const lazyRootSdk = lazyModule.moduleExports; + + expect(typeof lazyRootSdk.onDiagnosticEvent).toBe("function"); + expect(lazyRootSdk.onDiagnosticEvent).toBe(onDiagnosticEvent); + expect("onDiagnosticEvent" in lazyRootSdk).toBe(true); + }); + it("loads legacy root exports through the merged root wrapper", { timeout: 240_000 }, () => { expect(typeof rootSdk.resolveControlCommandGate).toBe("function"); + expect(typeof rootSdk.onDiagnosticEvent).toBe("function"); expect(typeof rootSdk.default).toBe("object"); expect(rootSdk.default).toBe(rootSdk); expect(rootSdk.__esModule).toBe(true); @@ -173,9 +194,12 @@ describe("plugin-sdk root alias", () => { it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => { expect("resolveControlCommandGate" in rootSdk).toBe(true); + expect("onDiagnosticEvent" in rootSdk).toBe(true); const keys = Object.keys(rootSdk); expect(keys).toContain("resolveControlCommandGate"); + expect(keys).toContain("onDiagnosticEvent"); const descriptor = Object.getOwnPropertyDescriptor(rootSdk, "resolveControlCommandGate"); expect(descriptor).toBeDefined(); + expect(Object.getOwnPropertyDescriptor(rootSdk, "onDiagnosticEvent")).toBeDefined(); }); }); diff --git a/src/plugin-sdk/runtime-api-guardrails.test.ts b/src/plugin-sdk/runtime-api-guardrails.test.ts index 78a39d7ccb3..2158edff7d0 100644 --- a/src/plugin-sdk/runtime-api-guardrails.test.ts +++ b/src/plugin-sdk/runtime-api-guardrails.test.ts @@ -38,6 +38,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { "extensions/matrix/runtime-api.ts": [ 'export * from "./src/auth-precedence.js";', 'export * from "./helper-api.js";', + 'export * from "./thread-bindings-runtime.js";', ], "extensions/nextcloud-talk/runtime-api.ts": [ 'export * from "../../src/plugin-sdk/nextcloud-talk.js";', diff --git a/src/plugin-sdk/telegram.ts b/src/plugin-sdk/telegram.ts index 4b1d41df386..6a579af19f4 100644 --- a/src/plugin-sdk/telegram.ts +++ b/src/plugin-sdk/telegram.ts @@ -1,127 +1 @@ -export type { - ChannelAccountSnapshot, - ChannelGatewayContext, - ChannelMessageActionAdapter, - ChannelPlugin, -} from "../channels/plugins/types.js"; -export type { OpenClawConfig } from "../config/config.js"; -export type { PluginRuntime } from "../plugins/runtime/types.js"; -export type { OpenClawPluginApi } from "../plugins/types.js"; -export type { - TelegramAccountConfig, - TelegramActionConfig, - TelegramNetworkConfig, -} from "../config/types.js"; -export type { - ChannelConfiguredBindingProvider, - ChannelConfiguredBindingConversationRef, - ChannelConfiguredBindingMatch, -} from "../channels/plugins/types.adapters.js"; -export type { InspectedTelegramAccount } from "../../extensions/telegram/api.js"; -export type { ResolvedTelegramAccount } from "../../extensions/telegram/api.js"; -export type { TelegramProbe } from "../../extensions/telegram/runtime-api.js"; -export type { TelegramButtonStyle, TelegramInlineButtons } from "../../extensions/telegram/api.js"; -export type { StickerMetadata } from "../../extensions/telegram/api.js"; - -export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; -export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; -export { parseTelegramTopicConversation } from "../acp/conversation-id.js"; -export { clearAccountEntryFields } from "../channels/plugins/config-helpers.js"; -export { resolveTelegramPollVisibility } from "../poll-params.js"; - -export { - PAIRING_APPROVED_MESSAGE, - applyAccountNameToChannelSection, - buildChannelConfigSchema, - deleteAccountFromConfigSection, - formatPairingApproveHint, - getChatChannelMeta, - migrateBaseNameToDefaultAccount, - setAccountEnabledInConfigSection, -} from "./channel-plugin-common.js"; - -export { - projectCredentialSnapshotFields, - resolveConfiguredFromCredentialStatuses, -} from "../channels/account-snapshot-fields.js"; -export { - resolveAllowlistProviderRuntimeGroupPolicy, - resolveDefaultGroupPolicy, -} from "../config/runtime-group-policy.js"; -export { - listTelegramDirectoryGroupsFromConfig, - listTelegramDirectoryPeersFromConfig, -} from "../../extensions/telegram/api.js"; -export { - resolveTelegramGroupRequireMention, - resolveTelegramGroupToolPolicy, -} from "../../extensions/telegram/api.js"; -export { TelegramConfigSchema } from "../config/zod-schema.providers-core.js"; - -export { buildTokenChannelStatusSummary } from "./status-helpers.js"; - -export { - createTelegramActionGate, - listTelegramAccountIds, - resolveDefaultTelegramAccountId, - resolveTelegramPollActionGateState, -} from "../../extensions/telegram/api.js"; -export { inspectTelegramAccount } from "../../extensions/telegram/api.js"; -export { - looksLikeTelegramTargetId, - normalizeTelegramMessagingTarget, -} from "../../extensions/telegram/api.js"; -export { - parseTelegramReplyToMessageId, - parseTelegramThreadId, -} from "../../extensions/telegram/api.js"; -export { - isNumericTelegramUserId, - normalizeTelegramAllowFromEntry, -} from "../../extensions/telegram/api.js"; -export { fetchTelegramChatId } from "../../extensions/telegram/api.js"; -export { - resolveTelegramInlineButtonsScope, - resolveTelegramTargetChatType, -} from "../../extensions/telegram/api.js"; -export { resolveTelegramReactionLevel } from "../../extensions/telegram/api.js"; -export { - auditTelegramGroupMembership, - collectTelegramUnmentionedGroupIds, - createForumTopicTelegram, - deleteMessageTelegram, - editForumTopicTelegram, - editMessageReplyMarkupTelegram, - editMessageTelegram, - monitorTelegramProvider, - pinMessageTelegram, - reactMessageTelegram, - renameForumTopicTelegram, - probeTelegram, - sendMessageTelegram, - sendPollTelegram, - sendStickerTelegram, - sendTypingTelegram, - unpinMessageTelegram, -} from "../../extensions/telegram/runtime-api.js"; -export { getCacheStats, searchStickers } from "../../extensions/telegram/api.js"; -export { resolveTelegramToken } from "../../extensions/telegram/runtime-api.js"; -export { telegramMessageActions } from "../../extensions/telegram/runtime-api.js"; -export { - setTelegramThreadBindingIdleTimeoutBySessionKey, - setTelegramThreadBindingMaxAgeBySessionKey, -} from "../../extensions/telegram/runtime-api.js"; -export { collectTelegramStatusIssues } from "../../extensions/telegram/api.js"; -export { sendTelegramPayloadMessages } from "../../extensions/telegram/api.js"; -export { - buildBrowseProvidersButton, - buildModelsKeyboard, - buildProviderKeyboard, - calculateTotalPages, - getModelsPageSize, - type ProviderInfo, -} from "../../extensions/telegram/api.js"; -export { - isTelegramExecApprovalApprover, - isTelegramExecApprovalClientEnabled, -} from "../../extensions/telegram/api.js"; +export * from "../plugins/runtime/runtime-telegram-contract.js"; diff --git a/src/plugin-sdk/text-runtime.ts b/src/plugin-sdk/text-runtime.ts index bfdb2db690f..5dd70cdcc3c 100644 --- a/src/plugin-sdk/text-runtime.ts +++ b/src/plugin-sdk/text-runtime.ts @@ -13,6 +13,7 @@ export * from "../shared/global-singleton.js"; export * from "../shared/string-normalization.js"; export * from "../shared/string-sample.js"; export * from "../shared/text/assistant-visible-text.js"; +export * from "../shared/text/auto-linked-file-ref.js"; export * from "../shared/text/code-regions.js"; export * from "../shared/text/reasoning-tags.js"; export * from "../terminal/safe-text.js"; diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index d996c42380c..725cac63849 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -194,7 +194,7 @@ const hasExplicitMemorySlot = (plugins?: OpenClawConfig["plugins"]) => const hasExplicitMemoryEntry = (plugins?: OpenClawConfig["plugins"]) => Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core")); -const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) => { +export const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) => { if (!plugins) { return false; } diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index e966e9d4128..7c69aa7ca41 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -4,6 +4,7 @@ import { withBundledPluginAllowlistCompat, withBundledPluginEnablementCompat, } from "./bundled-compat.js"; +import { hasExplicitPluginConfig } from "./config-state.js"; import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js"; import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; @@ -12,39 +13,17 @@ import type { ProviderPlugin } from "./types.js"; const log = createSubsystemLogger("plugins"); -function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { - const plugins = config?.plugins; - if (!plugins) { - return false; - } - if (typeof plugins.enabled === "boolean") { - return true; - } - if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { - return true; - } - if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { - return true; - } - if (Array.isArray(plugins.load?.paths) && plugins.load.paths.length > 0) { - return true; - } - if (plugins.entries && Object.keys(plugins.entries).length > 0) { - return true; - } - if (plugins.slots && Object.keys(plugins.slots).length > 0) { - return true; - } - return false; -} - function withBundledProviderVitestCompat(params: { config: PluginLoadOptions["config"]; pluginIds: readonly string[]; env?: PluginLoadOptions["env"]; }): PluginLoadOptions["config"] { const env = params.env ?? process.env; - if (!env.VITEST || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) { + if ( + !env.VITEST || + hasExplicitPluginConfig(params.config?.plugins) || + params.pluginIds.length === 0 + ) { return params.config; } diff --git a/src/plugins/runtime/runtime-matrix-contract.ts b/src/plugins/runtime/runtime-matrix-contract.ts new file mode 100644 index 00000000000..ec33e96ef2f --- /dev/null +++ b/src/plugins/runtime/runtime-matrix-contract.ts @@ -0,0 +1,178 @@ +// Narrow plugin-sdk surface for the bundled matrix plugin. +// Keep this list additive and scoped to symbols used under extensions/matrix. + +import { createOptionalChannelSetupSurface } from "../../plugin-sdk/channel-setup.js"; + +export { + createActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringArrayParam, + readStringParam, +} from "../../agents/tools/common.js"; +export type { ReplyPayload } from "../../auto-reply/types.js"; +export { resolveAckReaction } from "../../agents/identity.js"; +export { + compileAllowlist, + resolveCompiledAllowlistMatch, + resolveAllowlistCandidates, + resolveAllowlistMatchByCandidates, +} from "../../channels/allowlist-match.js"; +export { + addAllowlistUserEntriesFromConfigEntry, + buildAllowlistResolutionSummary, + canonicalizeAllowlistWithResolvedIds, + mergeAllowlist, + patchAllowlistUsersInConfigEntries, + summarizeMapping, +} from "../../channels/allowlists/resolve-utils.js"; +export { ensureConfiguredAcpBindingReady } from "../../acp/persistent-bindings.lifecycle.js"; +export { resolveConfiguredAcpBindingRecord } from "../../acp/persistent-bindings.resolve.js"; +export { resolveControlCommandGate } from "../../channels/command-gating.js"; +export type { NormalizedLocation } from "../../channels/location.js"; +export { formatLocationText, toLocationContext } from "../../channels/location.js"; +export { logInboundDrop, logTypingFailure } from "../../channels/logging.js"; +export type { AllowlistMatch } from "../../channels/plugins/allowlist-match.js"; +export { formatAllowlistMatchMeta } from "../../channels/plugins/allowlist-match.js"; +export { + buildChannelKeyCandidates, + resolveChannelEntryMatch, +} from "../../channels/plugins/channel-config.js"; +export { createAccountListHelpers } from "../../channels/plugins/account-helpers.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../../channels/plugins/helpers.js"; +export { + buildSingleChannelSecretPromptState, + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, + promptSingleChannelSecretInput, + setTopLevelChannelGroupPolicy, +} from "../../channels/plugins/setup-wizard-helpers.js"; +export { promptChannelAccessConfig } from "../../channels/plugins/setup-group-access.js"; +export { PAIRING_APPROVED_MESSAGE } from "../../channels/plugins/pairing-message.js"; +export { + applyAccountNameToChannelSection, + moveSingleAccountChannelSectionToDefaultAccount, +} from "../../channels/plugins/setup-helpers.js"; +export type { + BaseProbeResult, + ChannelDirectoryEntry, + ChannelGroupContext, + ChannelMessageActionAdapter, + ChannelMessageActionContext, + ChannelMessageActionName, + ChannelMessageToolDiscovery, + ChannelMessageToolSchemaContribution, + ChannelOutboundAdapter, + ChannelResolveKind, + ChannelResolveResult, + ChannelSetupInput, + ChannelToolSend, +} from "../../channels/plugins/types.js"; +export type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; +export { resolveThreadBindingFarewellText } from "../../channels/thread-bindings-messages.js"; +export { + resolveThreadBindingIdleTimeoutMsForChannel, + resolveThreadBindingMaxAgeMsForChannel, +} from "../../channels/thread-bindings-policy.js"; +export { + setMatrixThreadBindingIdleTimeoutBySessionKey, + setMatrixThreadBindingMaxAgeBySessionKey, +} from "../../../extensions/matrix/runtime-api.js"; +export { createTypingCallbacks } from "../../channels/typing.js"; +export { createChannelReplyPipeline } from "../../plugin-sdk/channel-reply-pipeline.js"; +export type { OpenClawConfig } from "../../config/config.js"; +export { + GROUP_POLICY_BLOCKED_LABEL, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../../config/runtime-group-policy.js"; +export type { + DmPolicy, + GroupPolicy, + GroupToolPolicyConfig, + MarkdownTableMode, +} from "../../config/types.js"; +export type { SecretInput } from "../../plugin-sdk/secret-input.js"; +export { + buildSecretInputSchema, + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../../plugin-sdk/secret-input.js"; +export { ToolPolicySchema } from "../../config/zod-schema.agent-runtime.js"; +export { MarkdownConfigSchema } from "../../config/zod-schema.core.js"; +export { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js"; +export { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js"; +export { maybeCreateMatrixMigrationSnapshot } from "../../infra/matrix-migration-snapshot.js"; +export { + getSessionBindingService, + registerSessionBindingAdapter, + unregisterSessionBindingAdapter, +} from "../../infra/outbound/session-binding-service.js"; +export { resolveOutboundSendDep } from "../../infra/outbound/send-deps.js"; +export type { + BindingTargetKind, + SessionBindingRecord, +} from "../../infra/outbound/session-binding-service.js"; +export { isPrivateOrLoopbackHost } from "../../gateway/net.js"; +export { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js"; +export { emptyPluginConfigSchema } from "../config-schema.js"; +export type { PluginRuntime, RuntimeLogger } from "./types.js"; +export type { OpenClawPluginApi } from "../types.js"; +export type { PollInput } from "../../polls.js"; +export { normalizePollInput } from "../../polls.js"; +export { + DEFAULT_ACCOUNT_ID, + normalizeAccountId, + normalizeOptionalAccountId, + resolveAgentIdFromSessionKey, +} from "../../routing/session-key.js"; +export type { RuntimeEnv } from "../../runtime.js"; +export { normalizeStringEntries } from "../../shared/string-normalization.js"; +export { formatDocsLink } from "../../terminal/links.js"; +export { redactSensitiveText } from "../../logging/redact.js"; +export type { WizardPrompter } from "../../wizard/prompts.js"; +export { + evaluateGroupRouteAccessForPolicy, + resolveSenderScopedGroupPolicy, +} from "../../plugin-sdk/group-access.js"; +export { createChannelPairingController } from "../../plugin-sdk/channel-pairing.js"; +export { readJsonFileWithFallback, writeJsonFileAtomically } from "../../plugin-sdk/json-store.js"; +export { formatResolvedUnresolvedNote } from "../../plugin-sdk/resolution-notes.js"; +export { runPluginCommandWithTimeout } from "../../plugin-sdk/run-command.js"; +export { createLoggerBackedRuntime, resolveRuntimeEnv } from "../../plugin-sdk/runtime.js"; +export { dispatchReplyFromConfigWithSettledDispatcher } from "../../plugin-sdk/inbound-reply-dispatch.js"; +export { + buildProbeChannelStatusSummary, + collectStatusIssuesFromLastError, +} from "../../plugin-sdk/status-helpers.js"; +export { + resolveMatrixAccountStorageRoot, + resolveMatrixCredentialsDir, + resolveMatrixCredentialsPath, + resolveMatrixLegacyFlatStoragePaths, +} from "../../../extensions/matrix/runtime-api.js"; +export { getMatrixScopedEnvVarNames } from "../../../extensions/matrix/runtime-api.js"; +export { + requiresExplicitMatrixDefaultAccount, + resolveMatrixDefaultOrOnlyAccountId, +} from "../../../extensions/matrix/runtime-api.js"; + +const matrixSetup = createOptionalChannelSetupSurface({ + channel: "matrix", + label: "Matrix", + npmSpec: "@openclaw/matrix", + docsPath: "/channels/matrix", +}); + +export const matrixSetupWizard = matrixSetup.setupWizard; +export const matrixSetupAdapter = matrixSetup.setupAdapter; diff --git a/src/plugins/runtime/runtime-telegram-contract.ts b/src/plugins/runtime/runtime-telegram-contract.ts new file mode 100644 index 00000000000..6700ae25429 --- /dev/null +++ b/src/plugins/runtime/runtime-telegram-contract.ts @@ -0,0 +1,130 @@ +export type { + ChannelAccountSnapshot, + ChannelGatewayContext, + ChannelMessageActionAdapter, +} from "../../channels/plugins/types.js"; +export type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; +export type { OpenClawConfig } from "../../config/config.js"; +export type { PluginRuntime } from "./types.js"; +export type { OpenClawPluginApi } from "../types.js"; +export type { + TelegramAccountConfig, + TelegramActionConfig, + TelegramNetworkConfig, +} from "../../config/types.js"; +export type { + ChannelConfiguredBindingProvider, + ChannelConfiguredBindingConversationRef, + ChannelConfiguredBindingMatch, +} from "../../channels/plugins/types.adapters.js"; +export type { InspectedTelegramAccount } from "../../../extensions/telegram/api.js"; +export type { ResolvedTelegramAccount } from "../../../extensions/telegram/api.js"; +export type { TelegramProbe } from "../../../extensions/telegram/runtime-api.js"; +export type { + TelegramButtonStyle, + TelegramInlineButtons, +} from "../../../extensions/telegram/api.js"; +export type { StickerMetadata } from "../../../extensions/telegram/api.js"; + +export { emptyPluginConfigSchema } from "../config-schema.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; +export { parseTelegramTopicConversation } from "../../acp/conversation-id.js"; +export { clearAccountEntryFields } from "../../channels/plugins/config-helpers.js"; +export { resolveTelegramPollVisibility } from "../../poll-params.js"; + +export { + PAIRING_APPROVED_MESSAGE, + applyAccountNameToChannelSection, + buildChannelConfigSchema, + deleteAccountFromConfigSection, + formatPairingApproveHint, + getChatChannelMeta, + migrateBaseNameToDefaultAccount, + setAccountEnabledInConfigSection, +} from "../../plugin-sdk/channel-plugin-common.js"; + +export { + projectCredentialSnapshotFields, + resolveConfiguredFromCredentialStatuses, +} from "../../channels/account-snapshot-fields.js"; +export { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, +} from "../../config/runtime-group-policy.js"; +export { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "../../../extensions/telegram/api.js"; +export { + resolveTelegramGroupRequireMention, + resolveTelegramGroupToolPolicy, +} from "../../../extensions/telegram/api.js"; +export { TelegramConfigSchema } from "../../config/zod-schema.providers-core.js"; + +export { buildTokenChannelStatusSummary } from "../../plugin-sdk/status-helpers.js"; + +export { + createTelegramActionGate, + listTelegramAccountIds, + resolveDefaultTelegramAccountId, + resolveTelegramPollActionGateState, +} from "../../../extensions/telegram/api.js"; +export { inspectTelegramAccount } from "../../../extensions/telegram/api.js"; +export { + looksLikeTelegramTargetId, + normalizeTelegramMessagingTarget, +} from "../../../extensions/telegram/api.js"; +export { + parseTelegramReplyToMessageId, + parseTelegramThreadId, +} from "../../../extensions/telegram/api.js"; +export { + isNumericTelegramUserId, + normalizeTelegramAllowFromEntry, +} from "../../../extensions/telegram/api.js"; +export { fetchTelegramChatId } from "../../../extensions/telegram/api.js"; +export { + resolveTelegramInlineButtonsScope, + resolveTelegramTargetChatType, +} from "../../../extensions/telegram/api.js"; +export { resolveTelegramReactionLevel } from "../../../extensions/telegram/api.js"; +export { + auditTelegramGroupMembership, + collectTelegramUnmentionedGroupIds, + createForumTopicTelegram, + deleteMessageTelegram, + editForumTopicTelegram, + editMessageReplyMarkupTelegram, + editMessageTelegram, + monitorTelegramProvider, + pinMessageTelegram, + reactMessageTelegram, + renameForumTopicTelegram, + probeTelegram, + sendMessageTelegram, + sendPollTelegram, + sendStickerTelegram, + sendTypingTelegram, + unpinMessageTelegram, +} from "../../../extensions/telegram/runtime-api.js"; +export { getCacheStats, searchStickers } from "../../../extensions/telegram/api.js"; +export { resolveTelegramToken } from "../../../extensions/telegram/runtime-api.js"; +export { telegramMessageActions } from "../../../extensions/telegram/runtime-api.js"; +export { + setTelegramThreadBindingIdleTimeoutBySessionKey, + setTelegramThreadBindingMaxAgeBySessionKey, +} from "../../../extensions/telegram/runtime-api.js"; +export { collectTelegramStatusIssues } from "../../../extensions/telegram/api.js"; +export { sendTelegramPayloadMessages } from "../../../extensions/telegram/api.js"; +export { + buildBrowseProvidersButton, + buildModelsKeyboard, + buildProviderKeyboard, + calculateTotalPages, + getModelsPageSize, + type ProviderInfo, +} from "../../../extensions/telegram/api.js"; +export { + isTelegramExecApprovalApprover, + isTelegramExecApprovalClientEnabled, +} from "../../../extensions/telegram/api.js"; diff --git a/src/plugins/runtime/types-channel.ts b/src/plugins/runtime/types-channel.ts index 1a44e0e45f1..5712f50eb31 100644 --- a/src/plugins/runtime/types-channel.ts +++ b/src/plugins/runtime/types-channel.ts @@ -94,29 +94,29 @@ export type PluginRuntimeChannel = { shouldHandleTextCommands: typeof import("../../auto-reply/commands-registry.js").shouldHandleTextCommands; }; discord: { - messageActions: typeof import("../../../extensions/discord/runtime-api.js").discordMessageActions; - auditChannelPermissions: typeof import("../../../extensions/discord/runtime-api.js").auditDiscordChannelPermissions; - listDirectoryGroupsLive: typeof import("../../../extensions/discord/runtime-api.js").listDiscordDirectoryGroupsLive; - listDirectoryPeersLive: typeof import("../../../extensions/discord/runtime-api.js").listDiscordDirectoryPeersLive; - probeDiscord: typeof import("../../../extensions/discord/runtime-api.js").probeDiscord; - resolveChannelAllowlist: typeof import("../../../extensions/discord/runtime-api.js").resolveDiscordChannelAllowlist; - resolveUserAllowlist: typeof import("../../../extensions/discord/runtime-api.js").resolveDiscordUserAllowlist; - sendComponentMessage: typeof import("../../../extensions/discord/runtime-api.js").sendDiscordComponentMessage; - sendMessageDiscord: typeof import("../../../extensions/discord/runtime-api.js").sendMessageDiscord; - sendPollDiscord: typeof import("../../../extensions/discord/runtime-api.js").sendPollDiscord; - monitorDiscordProvider: typeof import("../../../extensions/discord/runtime-api.js").monitorDiscordProvider; + messageActions: typeof import("../../plugin-sdk/discord.js").discordMessageActions; + auditChannelPermissions: typeof import("../../plugin-sdk/discord.js").auditDiscordChannelPermissions; + listDirectoryGroupsLive: typeof import("../../plugin-sdk/discord.js").listDiscordDirectoryGroupsLive; + listDirectoryPeersLive: typeof import("../../plugin-sdk/discord.js").listDiscordDirectoryPeersLive; + probeDiscord: typeof import("../../plugin-sdk/discord.js").probeDiscord; + resolveChannelAllowlist: typeof import("../../plugin-sdk/discord.js").resolveDiscordChannelAllowlist; + resolveUserAllowlist: typeof import("../../plugin-sdk/discord.js").resolveDiscordUserAllowlist; + sendComponentMessage: typeof import("../../plugin-sdk/discord.js").sendDiscordComponentMessage; + sendMessageDiscord: typeof import("../../plugin-sdk/discord.js").sendMessageDiscord; + sendPollDiscord: typeof import("../../plugin-sdk/discord.js").sendPollDiscord; + monitorDiscordProvider: typeof import("../../plugin-sdk/discord.js").monitorDiscordProvider; threadBindings: { - getManager: typeof import("../../../extensions/discord/runtime-api.js").getThreadBindingManager; - resolveIdleTimeoutMs: typeof import("../../../extensions/discord/runtime-api.js").resolveThreadBindingIdleTimeoutMs; - resolveInactivityExpiresAt: typeof import("../../../extensions/discord/runtime-api.js").resolveThreadBindingInactivityExpiresAt; - resolveMaxAgeMs: typeof import("../../../extensions/discord/runtime-api.js").resolveThreadBindingMaxAgeMs; - resolveMaxAgeExpiresAt: typeof import("../../../extensions/discord/runtime-api.js").resolveThreadBindingMaxAgeExpiresAt; - setIdleTimeoutBySessionKey: typeof import("../../../extensions/discord/runtime-api.js").setThreadBindingIdleTimeoutBySessionKey; - setMaxAgeBySessionKey: typeof import("../../../extensions/discord/runtime-api.js").setThreadBindingMaxAgeBySessionKey; - unbindBySessionKey: typeof import("../../../extensions/discord/runtime-api.js").unbindThreadBindingsBySessionKey; + getManager: typeof import("../../plugin-sdk/discord.js").getThreadBindingManager; + resolveIdleTimeoutMs: typeof import("../../plugin-sdk/discord.js").resolveThreadBindingIdleTimeoutMs; + resolveInactivityExpiresAt: typeof import("../../plugin-sdk/discord.js").resolveThreadBindingInactivityExpiresAt; + resolveMaxAgeMs: typeof import("../../plugin-sdk/discord.js").resolveThreadBindingMaxAgeMs; + resolveMaxAgeExpiresAt: typeof import("../../plugin-sdk/discord.js").resolveThreadBindingMaxAgeExpiresAt; + setIdleTimeoutBySessionKey: typeof import("../../plugin-sdk/discord.js").setThreadBindingIdleTimeoutBySessionKey; + setMaxAgeBySessionKey: typeof import("../../plugin-sdk/discord.js").setThreadBindingMaxAgeBySessionKey; + unbindBySessionKey: typeof import("../../plugin-sdk/discord.js").unbindThreadBindingsBySessionKey; }; typing: { - pulse: typeof import("../../../extensions/discord/runtime-api.js").sendTypingDiscord; + pulse: typeof import("../../plugin-sdk/discord.js").sendTypingDiscord; start: (params: { channelId: string; accountId?: string; @@ -128,39 +128,39 @@ export type PluginRuntimeChannel = { }>; }; conversationActions: { - editMessage: typeof import("../../../extensions/discord/runtime-api.js").editMessageDiscord; - deleteMessage: typeof import("../../../extensions/discord/runtime-api.js").deleteMessageDiscord; - pinMessage: typeof import("../../../extensions/discord/runtime-api.js").pinMessageDiscord; - unpinMessage: typeof import("../../../extensions/discord/runtime-api.js").unpinMessageDiscord; - createThread: typeof import("../../../extensions/discord/runtime-api.js").createThreadDiscord; - editChannel: typeof import("../../../extensions/discord/runtime-api.js").editChannelDiscord; + editMessage: typeof import("../../plugin-sdk/discord.js").editMessageDiscord; + deleteMessage: typeof import("../../plugin-sdk/discord.js").deleteMessageDiscord; + pinMessage: typeof import("../../plugin-sdk/discord.js").pinMessageDiscord; + unpinMessage: typeof import("../../plugin-sdk/discord.js").unpinMessageDiscord; + createThread: typeof import("../../plugin-sdk/discord.js").createThreadDiscord; + editChannel: typeof import("../../plugin-sdk/discord.js").editChannelDiscord; }; }; slack: { - listDirectoryGroupsLive: typeof import("../../../extensions/slack/runtime-api.js").listSlackDirectoryGroupsLive; - listDirectoryPeersLive: typeof import("../../../extensions/slack/runtime-api.js").listSlackDirectoryPeersLive; - probeSlack: typeof import("../../../extensions/slack/runtime-api.js").probeSlack; - resolveChannelAllowlist: typeof import("../../../extensions/slack/runtime-api.js").resolveSlackChannelAllowlist; - resolveUserAllowlist: typeof import("../../../extensions/slack/runtime-api.js").resolveSlackUserAllowlist; - sendMessageSlack: typeof import("../../../extensions/slack/runtime-api.js").sendMessageSlack; - monitorSlackProvider: typeof import("../../../extensions/slack/runtime-api.js").monitorSlackProvider; - handleSlackAction: typeof import("../../../extensions/slack/runtime-api.js").handleSlackAction; + listDirectoryGroupsLive: typeof import("../../plugin-sdk/slack.js").listSlackDirectoryGroupsLive; + listDirectoryPeersLive: typeof import("../../plugin-sdk/slack.js").listSlackDirectoryPeersLive; + probeSlack: typeof import("../../plugin-sdk/slack.js").probeSlack; + resolveChannelAllowlist: typeof import("../../plugin-sdk/slack.js").resolveSlackChannelAllowlist; + resolveUserAllowlist: typeof import("../../plugin-sdk/slack.js").resolveSlackUserAllowlist; + sendMessageSlack: typeof import("../../plugin-sdk/slack.js").sendMessageSlack; + monitorSlackProvider: typeof import("../../plugin-sdk/slack.js").monitorSlackProvider; + handleSlackAction: typeof import("../../plugin-sdk/slack.js").handleSlackAction; }; telegram: { - auditGroupMembership: typeof import("../../../extensions/telegram/runtime-api.js").auditTelegramGroupMembership; - collectUnmentionedGroupIds: typeof import("../../../extensions/telegram/runtime-api.js").collectTelegramUnmentionedGroupIds; - probeTelegram: typeof import("../../../extensions/telegram/runtime-api.js").probeTelegram; - resolveTelegramToken: typeof import("../../../extensions/telegram/runtime-api.js").resolveTelegramToken; - sendMessageTelegram: typeof import("../../../extensions/telegram/runtime-api.js").sendMessageTelegram; - sendPollTelegram: typeof import("../../../extensions/telegram/runtime-api.js").sendPollTelegram; - monitorTelegramProvider: typeof import("../../../extensions/telegram/runtime-api.js").monitorTelegramProvider; - messageActions: typeof import("../../../extensions/telegram/runtime-api.js").telegramMessageActions; + auditGroupMembership: typeof import("../../plugin-sdk/telegram.js").auditTelegramGroupMembership; + collectUnmentionedGroupIds: typeof import("../../plugin-sdk/telegram.js").collectTelegramUnmentionedGroupIds; + probeTelegram: typeof import("../../plugin-sdk/telegram.js").probeTelegram; + resolveTelegramToken: typeof import("../../plugin-sdk/telegram.js").resolveTelegramToken; + sendMessageTelegram: typeof import("../../plugin-sdk/telegram.js").sendMessageTelegram; + sendPollTelegram: typeof import("../../plugin-sdk/telegram.js").sendPollTelegram; + monitorTelegramProvider: typeof import("../../plugin-sdk/telegram.js").monitorTelegramProvider; + messageActions: typeof import("../../plugin-sdk/telegram.js").telegramMessageActions; threadBindings: { - setIdleTimeoutBySessionKey: typeof import("../../../extensions/telegram/runtime-api.js").setTelegramThreadBindingIdleTimeoutBySessionKey; - setMaxAgeBySessionKey: typeof import("../../../extensions/telegram/runtime-api.js").setTelegramThreadBindingMaxAgeBySessionKey; + setIdleTimeoutBySessionKey: typeof import("../../plugin-sdk/telegram.js").setTelegramThreadBindingIdleTimeoutBySessionKey; + setMaxAgeBySessionKey: typeof import("../../plugin-sdk/telegram.js").setTelegramThreadBindingMaxAgeBySessionKey; }; typing: { - pulse: typeof import("../../../extensions/telegram/runtime-api.js").sendTypingTelegram; + pulse: typeof import("../../plugin-sdk/telegram.js").sendTypingTelegram; start: (params: { to: string; accountId?: string; @@ -173,8 +173,8 @@ export type PluginRuntimeChannel = { }>; }; conversationActions: { - editMessage: typeof import("../../../extensions/telegram/runtime-api.js").editMessageTelegram; - editReplyMarkup: typeof import("../../../extensions/telegram/runtime-api.js").editMessageReplyMarkupTelegram; + editMessage: typeof import("../../plugin-sdk/telegram.js").editMessageTelegram; + editReplyMarkup: typeof import("../../plugin-sdk/telegram.js").editMessageReplyMarkupTelegram; clearReplyMarkup: ( chatIdInput: string | number, messageIdInput: string | number, @@ -187,10 +187,10 @@ export type PluginRuntimeChannel = { cfg?: ReturnType; }, ) => Promise<{ ok: true; messageId: string; chatId: string }>; - deleteMessage: typeof import("../../../extensions/telegram/runtime-api.js").deleteMessageTelegram; - renameTopic: typeof import("../../../extensions/telegram/runtime-api.js").renameForumTopicTelegram; - pinMessage: typeof import("../../../extensions/telegram/runtime-api.js").pinMessageTelegram; - unpinMessage: typeof import("../../../extensions/telegram/runtime-api.js").unpinMessageTelegram; + deleteMessage: typeof import("../../plugin-sdk/telegram.js").deleteMessageTelegram; + renameTopic: typeof import("../../plugin-sdk/telegram.js").renameForumTopicTelegram; + pinMessage: typeof import("../../plugin-sdk/telegram.js").pinMessageTelegram; + unpinMessage: typeof import("../../plugin-sdk/telegram.js").unpinMessageTelegram; }; }; matrix: { @@ -200,15 +200,15 @@ export type PluginRuntimeChannel = { }; }; signal: { - probeSignal: typeof import("../../../extensions/signal/runtime-api.js").probeSignal; - sendMessageSignal: typeof import("../../../extensions/signal/runtime-api.js").sendMessageSignal; - monitorSignalProvider: typeof import("../../../extensions/signal/runtime-api.js").monitorSignalProvider; - messageActions: typeof import("../../../extensions/signal/runtime-api.js").signalMessageActions; + probeSignal: typeof import("../../plugin-sdk/signal.js").probeSignal; + sendMessageSignal: typeof import("../../plugin-sdk/signal.js").sendMessageSignal; + monitorSignalProvider: typeof import("../../plugin-sdk/signal.js").monitorSignalProvider; + messageActions: typeof import("../../plugin-sdk/signal.js").signalMessageActions; }; imessage: { - monitorIMessageProvider: typeof import("../../../extensions/imessage/runtime-api.js").monitorIMessageProvider; - probeIMessage: typeof import("../../../extensions/imessage/runtime-api.js").probeIMessage; - sendMessageIMessage: typeof import("../../../extensions/imessage/runtime-api.js").sendMessageIMessage; + monitorIMessageProvider: typeof import("../../plugin-sdk/imessage.js").monitorIMessageProvider; + probeIMessage: typeof import("../../plugin-sdk/imessage.js").probeIMessage; + sendMessageIMessage: typeof import("../../plugin-sdk/imessage.js").sendMessageIMessage; }; whatsapp: { getActiveWebListener: typeof import("./runtime-whatsapp-boundary.js").getActiveWebListener; diff --git a/src/plugins/web-search-providers.shared.ts b/src/plugins/web-search-providers.shared.ts index 29ba9527590..31a90f50915 100644 --- a/src/plugins/web-search-providers.shared.ts +++ b/src/plugins/web-search-providers.shared.ts @@ -3,36 +3,14 @@ import { withBundledPluginEnablementCompat, } from "./bundled-compat.js"; import { resolveBundledWebSearchPluginIds } from "./bundled-web-search.js"; -import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js"; +import { + hasExplicitPluginConfig, + normalizePluginsConfig, + type NormalizedPluginsConfig, +} from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; import type { PluginWebSearchProviderEntry } from "./types.js"; -export function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { - const plugins = config?.plugins; - if (!plugins) { - return false; - } - if (typeof plugins.enabled === "boolean") { - return true; - } - if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { - return true; - } - if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { - return true; - } - if (Array.isArray(plugins.load?.paths) && plugins.load.paths.length > 0) { - return true; - } - if (plugins.entries && Object.keys(plugins.entries).length > 0) { - return true; - } - if (plugins.slots && Object.keys(plugins.slots).length > 0) { - return true; - } - return false; -} - function resolveBundledWebSearchCompatPluginIds(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; @@ -52,7 +30,11 @@ function withBundledWebSearchVitestCompat(params: { }): PluginLoadOptions["config"] { const env = params.env ?? process.env; const isVitest = Boolean(env.VITEST || process.env.VITEST); - if (!isVitest || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) { + if ( + !isVitest || + hasExplicitPluginConfig(params.config?.plugins) || + params.pluginIds.length === 0 + ) { return params.config; } diff --git a/src/shared/assistant-error-format.ts b/src/shared/assistant-error-format.ts index 6564cf5c641..b07d5b2ac53 100644 --- a/src/shared/assistant-error-format.ts +++ b/src/shared/assistant-error-format.ts @@ -41,7 +41,7 @@ function isErrorPayloadObject(payload: unknown): payload is ErrorPayload { return false; } -function parseApiErrorPayload(raw: string): ErrorPayload | null { +export function parseApiErrorPayload(raw?: string): ErrorPayload | null { if (!raw) { return null; } diff --git a/src/shared/text/auto-linked-file-ref.ts b/src/shared/text/auto-linked-file-ref.ts new file mode 100644 index 00000000000..6fd5693202b --- /dev/null +++ b/src/shared/text/auto-linked-file-ref.ts @@ -0,0 +1,27 @@ +const FILE_REF_EXTENSIONS = ["md", "go", "py", "pl", "sh", "am", "at", "be", "cc"] as const; + +export const FILE_REF_EXTENSIONS_WITH_TLD = new Set(FILE_REF_EXTENSIONS); + +export function isAutoLinkedFileRef(href: string, label: string): boolean { + const stripped = href.replace(/^https?:\/\//i, ""); + if (stripped !== label) { + return false; + } + const dotIndex = label.lastIndexOf("."); + if (dotIndex < 1) { + return false; + } + const ext = label.slice(dotIndex + 1).toLowerCase(); + if (!FILE_REF_EXTENSIONS_WITH_TLD.has(ext)) { + return false; + } + const segments = label.split("/"); + if (segments.length > 1) { + for (let i = 0; i < segments.length - 1; i += 1) { + if (segments[i]?.includes(".")) { + return false; + } + } + } + return true; +} diff --git a/test/plugin-extension-import-boundary.test.ts b/test/plugin-extension-import-boundary.test.ts index 254b3613797..c2bd07b5e00 100644 --- a/test/plugin-extension-import-boundary.test.ts +++ b/test/plugin-extension-import-boundary.test.ts @@ -29,13 +29,16 @@ describe("plugin extension import boundary inventory", () => { ); }); - it("ignores plugin-sdk boundary shims by scope", async () => { + it("ignores boundary shims by scope", async () => { const inventory = await collectPluginExtensionImportBoundaryInventory(); expect(inventory.some((entry) => entry.file.startsWith("src/plugin-sdk/"))).toBe(false); expect(inventory.some((entry) => entry.file.startsWith("src/plugin-sdk-internal/"))).toBe( false, ); + expect(inventory.some((entry) => entry.file.startsWith("src/plugins/runtime/runtime-"))).toBe( + false, + ); }); it("produces stable sorted output", async () => { diff --git a/tsdown.config.ts b/tsdown.config.ts index 746c6e883bc..98dd9e3d341 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -186,6 +186,8 @@ const coreDistEntries = buildCoreDistEntries(); function buildUnifiedDistEntries(): Record { return { ...coreDistEntries, + // Internal compat artifact for the root-alias.cjs lazy loader. + "plugin-sdk/compat": "src/plugin-sdk/compat.ts", ...Object.fromEntries( Object.entries(buildPluginSdkEntrySources()).map(([entry, source]) => [ `plugin-sdk/${entry}`,