Merge branch 'main' into feat/deepinfra-integration

This commit is contained in:
Georgi Atsev 2026-03-20 18:49:54 +02:00 committed by GitHub
commit 37220ed506
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1143 additions and 1240 deletions

View File

@ -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";

View File

@ -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<string, unknown>;
timeoutSeconds: number;
errorLabel: string;
}): Promise<Record<string, unknown>> {
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<string, unknown>;
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<string, unknown>): 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<string, unknown>;
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<string, unknown>,
);
const result = parseFirecrawlScrapePayload({
payload,
url: params.url,

View File

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

View File

@ -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<ResolvedLineAccount>,
"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";

View File

@ -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<ResolvedLineAccount> = {
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,
};

View File

@ -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<ResolvedLineAccount>({
channelKey: "line",
resolvePolicy: (account) => account.config.dmPolicy,
@ -63,10 +49,7 @@ const collectLineSecurityWarnings =
export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
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<ResolvedLineAccount> = {
});
},
}),
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,

View File

@ -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";

View File

@ -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";

View File

@ -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<NonNullable<typeof md.renderer.rules.link_open>>[0],
idx: number,

View File

@ -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(

View File

@ -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<string, unknown>; 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<string, unknown>;
timeoutSeconds: number;
errorLabel: string;
}): Promise<Record<string, unknown>> {
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<string, unknown>;
},
);
}
export async function runTavilySearch(
params: TavilySearchParams,
): Promise<Record<string, unknown>> {
@ -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<string, unknown>,
);
const rawResults = Array.isArray(payload.results) ? payload.results : [];
const results = rawResults.map((r: Record<string, unknown>) => ({
@ -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<string, unknown>,
);
const rawResults = Array.isArray(payload.results) ? payload.results : [];
const results = rawResults.map((r: Record<string, unknown>) => ({
@ -282,5 +249,5 @@ export async function runTavilyExtract(
}
export const __testing = {
postTavilyJson,
resolveEndpoint,
};

View File

@ -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 = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
const FILE_REFERENCE_PATTERN = new RegExp(
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`,

View File

@ -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,

View File

@ -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, unknown>): 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<string, unknown>) =>
resolveXaiWebSearchModel(grok ? { grok } : undefined),
resolveGrokInlineCitations: (grok?: Record<string, unknown>) =>
resolveXaiInlineCitations(grok ? { grok } : undefined),
extractGrokContent: extractXaiWebSearchContent,
extractXaiWebSearchContent,
resolveXaiInlineCitations,
resolveXaiSearchConfig,
resolveXaiWebSearchModel,
requestXaiWebSearch,
buildXaiWebSearchPayload,
} as const;

View File

@ -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<string, unknown> & {
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<string, unknown> {
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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
export function resolveXaiSearchConfig(searchConfig?: Record<string, unknown>): XaiWebSearchConfig {
return (asRecord(searchConfig?.grok) as XaiWebSearchConfig | undefined) ?? {};
}
export function resolveXaiWebSearchModel(searchConfig?: Record<string, unknown>): 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<string, unknown>): 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<XaiWebSearchResult> {
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;

View File

@ -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<string, unknown>; 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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function resolveXaiWebSearchConfig(
searchConfig?: Record<string, unknown>,
): Record<string, unknown> {
return asRecord(searchConfig?.grok) ?? {};
}
function resolveXaiWebSearchModel(searchConfig?: Record<string, unknown>): 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<string, unknown>): boolean {
return resolveXaiWebSearchConfig(searchConfig).inlineCitations === true;
}
function readQuery(args: Record<string, unknown>): string {
const value = typeof args.query === "string" ? args.query.trim() : "";
if (!value) {
throw new Error("query required");
}
return value;
}
function readCount(args: Record<string, unknown>): 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<never> {
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,
};

View File

@ -1 +1,5 @@
export * from "../../src/plugin-sdk/zai.js";
export {
detectZaiEndpoint,
type ZaiDetectedEndpoint,
type ZaiEndpointId,
} from "openclaw/plugin-sdk/provider-zai-endpoint";

View File

@ -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:<id>` 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:<id>)");
}
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<ResolvedZalouserAccount> = {
},
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;
}

View File

@ -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:<id>` 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:<id>)");
}
return normalized;
}
export function resolveZalouserOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
const normalized = normalizePrefixedTarget(params.target);
const normalized = normalizeZalouserTarget(params.target);
if (!normalized) {
return null;
}

View File

@ -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"

View File

@ -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() {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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");
});
});

View File

@ -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<string, unknown>;
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;

View File

@ -92,6 +92,45 @@ export async function withTrustedWebSearchEndpoint<T>(
);
}
export async function postTrustedWebToolsJson<T>(
params: {
url: string;
timeoutSeconds: number;
apiKey: string;
body: Record<string, unknown>;
errorLabel: string;
maxErrorBytes?: number;
},
parseResponse: (response: Response) => Promise<T>,
): Promise<T> {
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<never> {
const detailResult = await readResponseText(res, { maxBytes: 64_000 });
const detail = detailResult.text;

View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -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";

View File

@ -51,6 +51,8 @@ export type {
ProviderAuthMethodNonInteractiveContext,
ProviderAuthMethod,
ProviderAuthResult,
OpenClawPluginToolContext,
OpenClawPluginToolFactory,
OpenClawPluginCommandDefinition,
OpenClawPluginDefinition,
PluginCommandContext,

View File

@ -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 () => {

View File

@ -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";

View File

@ -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";

View File

@ -0,0 +1,6 @@
// Public provider auth environment variable helpers for plugin runtimes.
export {
listKnownProviderAuthEnvVarNames,
omitEnvKeysCaseInsensitive,
} from "../secrets/provider-env-vars.js";

View File

@ -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";

View File

@ -23,6 +23,7 @@ export {
resolveSearchCount,
resolveSearchTimeoutSeconds,
resolveSiteName,
postTrustedWebToolsJson,
throwWebSearchApiError,
withTrustedWebSearchEndpoint,
writeCachedSearchPayload,

View File

@ -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";

View File

@ -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.

View File

@ -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();
});
});

View File

@ -38,6 +38,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
"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";',

View File

@ -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";

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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";

View File

@ -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<typeof import("../../config/config.js").loadConfig>;
},
) => 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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<string>(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;
}

View File

@ -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 () => {

View File

@ -186,6 +186,8 @@ const coreDistEntries = buildCoreDistEntries();
function buildUnifiedDistEntries(): Record<string, string> {
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}`,