refactor: tighten plugin sdk entry surface

This commit is contained in:
Peter Steinberger
2026-03-21 20:07:24 +00:00
parent c29ba9d21a
commit bfcfc17a8b
49 changed files with 937 additions and 774 deletions

View File

@@ -128,7 +128,7 @@ my-plugin/
**Provider plugin:**
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "my-provider",
@@ -144,7 +144,7 @@ my-plugin/
**Multi-capability plugin** (provider + tool):
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "my-plugin",
@@ -157,8 +157,14 @@ my-plugin/
});
```
Use `defineChannelPluginEntry` for channel plugins and `definePluginEntry`
for everything else. A single plugin can register as many capabilities as needed.
Use `defineChannelPluginEntry` from `plugin-sdk/core` for channel plugins
and `definePluginEntry` from `plugin-sdk/plugin-entry` for everything else.
A single plugin can register as many capabilities as needed.
For chat-style channels, `plugin-sdk/core` also exposes
`createChatChannelPlugin(...)` so you can compose common DM security,
text pairing, reply threading, and attached outbound send results without
wiring each adapter separately.
</Step>
@@ -173,7 +179,7 @@ my-plugin/
```typescript
// Correct: focused subpaths
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth";
@@ -187,7 +193,8 @@ my-plugin/
<Accordion title="Common subpaths reference">
| Subpath | Purpose |
| --- | --- |
| `plugin-sdk/core` | Plugin entry definitions and base types |
| `plugin-sdk/plugin-entry` | Canonical `definePluginEntry` helper + provider/plugin entry types |
| `plugin-sdk/core` | Channel entry helpers, channel builders, and shared base types |
| `plugin-sdk/channel-setup` | Setup wizard adapters |
| `plugin-sdk/channel-pairing` | DM pairing primitives |
| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring |

View File

@@ -115,7 +115,8 @@ is a small, self-contained module with a clear purpose and documented contract.
<Accordion title="Full import path table">
| Import path | Purpose | Key exports |
| --- | --- | --- |
| `plugin-sdk/core` | Plugin entry definitions, base types | `defineChannelPluginEntry`, `definePluginEntry` |
| `plugin-sdk/plugin-entry` | Canonical plugin entry helper | `definePluginEntry` |
| `plugin-sdk/core` | Channel entry definitions, channel builders, base types | `defineChannelPluginEntry`, `createChatChannelPlugin` |
| `plugin-sdk/channel-setup` | Setup wizard adapters | `createOptionalChannelSetupSurface` |
| `plugin-sdk/channel-pairing` | DM pairing primitives | `createChannelPairingController` |
| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring | `createChannelReplyPipeline` |

View File

@@ -5,7 +5,7 @@ import {
type ProviderAuthContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/plugin-entry";
import {
CLAUDE_CLI_PROFILE_ID,
applyAuthProfileConfig,

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createBraveWebSearchProvider } from "./src/brave-web-search-provider.js";
export default definePluginEntry({

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-catalog.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
buildApiKeyCredential,

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createDiagnosticsOtelService } from "./src/service.js";
export default definePluginEntry({

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildElevenLabsSpeechProvider } from "openclaw/plugin-sdk/speech";
export default definePluginEntry({

View File

@@ -1,5 +1,5 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildFalImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { applyFalConfig, FAL_DEFAULT_IMAGE_MODEL_REF } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
import { createFirecrawlScrapeTool } from "./src/firecrawl-scrape-tool.js";
import { createFirecrawlWebSearchProvider } from "./src/firecrawl-search-provider.js";
import { createFirecrawlSearchTool } from "./src/firecrawl-search-tool.js";

View File

@@ -1,5 +1,5 @@
import { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin-sdk/agent-runtime";
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/core";
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
import { coerceSecretRef } from "openclaw/plugin-sdk/provider-auth";
import { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";

View File

@@ -1,3 +1,4 @@
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { googlechatPlugin } from "./src/channel.js";
import { setGoogleChatRuntime } from "./src/runtime.js";
@@ -9,6 +10,6 @@ export default defineChannelPluginEntry({
id: "googlechat",
name: "Google Chat",
description: "OpenClaw Google Chat channel plugin",
plugin: googlechatPlugin,
plugin: googlechatPlugin as ChannelPlugin,
setRuntime: setGoogleChatRuntime,
});

View File

@@ -1,17 +1,12 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createTextPairingAdapter } from "openclaw/plugin-sdk/channel-pairing";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import {
composeWarningCollectors,
createAllowlistProviderGroupPolicyWarningCollector,
createConditionalWarningCollector,
createAllowlistProviderOpenWarningCollector,
} from "openclaw/plugin-sdk/channel-policy";
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
import { createTopLevelChannelReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import {
createChannelDirectoryAdapter,
listResolvedDirectoryGroupEntriesFromMapKeys,
@@ -30,7 +25,6 @@ import {
resolveChannelMediaMaxBytes,
runPassiveAccountLifecycle,
type ChannelMessageActionAdapter,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
} from "../runtime-api.js";
@@ -92,14 +86,6 @@ const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleC
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
});
const resolveGoogleChatDmPolicy = createScopedDmSecurityResolver<ResolvedGoogleChatAccount>({
channelKey: "googlechat",
resolvePolicy: (account) => account.config.dm?.policy,
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
allowFromPathSuffix: "dm.",
normalizeEntry: (raw) => formatAllowFromEntry(raw),
});
const googlechatActions: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) => googlechatMessageActions.describeMessageTool?.(ctx) ?? null,
extractToolSend: (ctx) => googlechatMessageActions.extractToolSend?.(ctx) ?? null,
@@ -135,138 +121,258 @@ const collectGoogleChatSecurityWarnings = composeWarningCollectors<{
),
);
export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
id: "googlechat",
meta: { ...meta },
setup: googlechatSetupAdapter,
setupWizard: googlechatSetupWizard,
pairing: createTextPairingAdapter({
idLabel: "googlechatUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
notify: async ({ cfg, id, message }) => {
const account = resolveGoogleChatAccount({ cfg: cfg });
if (account.credentialSource === "none") {
return;
}
const user = normalizeGoogleChatTarget(id) ?? id;
const target = isGoogleChatUserTarget(user) ? user : `users/${user}`;
const space = await resolveGoogleChatOutboundSpace({ account, target });
const { sendGoogleChatMessage } = await loadGoogleChatChannelRuntime();
await sendGoogleChatMessage({
account,
space,
text: message,
});
export const googlechatPlugin = createChatChannelPlugin({
base: {
id: "googlechat",
meta: { ...meta },
setup: googlechatSetupAdapter,
setupWizard: googlechatSetupWizard,
capabilities: {
chatTypes: ["direct", "group", "thread"],
reactions: true,
threads: true,
media: true,
nativeCommands: false,
blockStreaming: true,
},
}),
capabilities: {
chatTypes: ["direct", "group", "thread"],
reactions: true,
threads: true,
media: true,
nativeCommands: false,
blockStreaming: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.googlechat"] },
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
config: {
...googleChatConfigAdapter,
isConfigured: (account) => account.credentialSource !== "none",
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
credentialSource: account.credentialSource,
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.googlechat"] },
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
config: {
...googleChatConfigAdapter,
isConfigured: (account) => account.credentialSource !== "none",
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
credentialSource: account.credentialSource,
}),
},
groups: {
resolveRequireMention: resolveGoogleChatGroupRequireMention,
},
messaging: {
normalizeTarget: normalizeGoogleChatTarget,
targetResolver: {
looksLikeId: (raw, normalized) => {
const value = normalized ?? raw.trim();
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
},
hint: "<spaces/{space}|users/{user}>",
},
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) =>
listResolvedDirectoryUserEntriesFromAllowFrom({
...params,
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry,
}),
listGroups: async (params) =>
listResolvedDirectoryGroupEntriesFromMapKeys({
...params,
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
resolveGroups: (account) => account.config.groups,
}),
}),
resolver: {
resolveTargets: async ({ inputs, kind }) => {
const resolved = inputs.map((input) => {
const normalized = normalizeGoogleChatTarget(input);
if (!normalized) {
return { input, resolved: false, note: "empty target" };
}
if (kind === "user" && isGoogleChatUserTarget(normalized)) {
return { input, resolved: true, id: normalized };
}
if (kind === "group" && isGoogleChatSpaceTarget(normalized)) {
return { input, resolved: true, id: normalized };
}
return {
input,
resolved: false,
note: "use spaces/{space} or users/{user}",
};
});
return resolved;
},
},
actions: googlechatActions,
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
accounts.flatMap((entry) => {
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
const enabled = entry.enabled !== false;
const configured = entry.configured === true;
if (!enabled || !configured) {
return [];
}
const issues: ChannelStatusIssue[] = [];
if (!entry.audience) {
issues.push({
channel: "googlechat",
accountId,
kind: "config",
message: "Google Chat audience is missing (set channels.googlechat.audience).",
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
});
}
if (!entry.audienceType) {
issues.push({
channel: "googlechat",
accountId,
kind: "config",
message: "Google Chat audienceType is missing (app-url or project-number).",
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
});
}
return issues;
}),
buildChannelSummary: ({ snapshot }) =>
buildPassiveProbedChannelStatusSummary(snapshot, {
credentialSource: snapshot.credentialSource ?? "none",
audienceType: snapshot.audienceType ?? null,
audience: snapshot.audience ?? null,
webhookPath: snapshot.webhookPath ?? null,
webhookUrl: snapshot.webhookUrl ?? null,
}),
probeAccount: async ({ account }) =>
(await loadGoogleChatChannelRuntime()).probeGoogleChat(account),
buildAccountSnapshot: ({ account, runtime, probe }) => {
const base = buildComputedAccountStatusSnapshot({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
runtime,
probe,
});
return {
...base,
credentialSource: account.credentialSource,
audienceType: account.config.audienceType,
audience: account.config.audience,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
dmPolicy: account.config.dm?.policy ?? "pairing",
};
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const statusSink = createAccountStatusSink({
accountId: account.accountId,
setStatus: ctx.setStatus,
});
ctx.log?.info(`[${account.accountId}] starting Google Chat webhook`);
const { resolveGoogleChatWebhookPath, startGoogleChatMonitor } =
await loadGoogleChatChannelRuntime();
statusSink({
running: true,
lastStartAt: Date.now(),
webhookPath: resolveGoogleChatWebhookPath({ account }),
audienceType: account.config.audienceType,
audience: account.config.audience,
});
await runPassiveAccountLifecycle({
abortSignal: ctx.abortSignal,
start: async () =>
await startGoogleChatMonitor({
account,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
statusSink,
}),
stop: async (unregister) => {
unregister?.();
},
onStop: async () => {
statusSink({
running: false,
lastStopAt: Date.now(),
});
},
});
},
},
},
pairing: {
text: {
idLabel: "googlechatUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
notify: async ({ cfg, id, message }) => {
const account = resolveGoogleChatAccount({ cfg: cfg });
if (account.credentialSource === "none") {
return;
}
const user = normalizeGoogleChatTarget(id) ?? id;
const target = isGoogleChatUserTarget(user) ? user : `users/${user}`;
const space = await resolveGoogleChatOutboundSpace({ account, target });
const { sendGoogleChatMessage } = await loadGoogleChatChannelRuntime();
await sendGoogleChatMessage({
account,
space,
text: message,
});
},
},
},
security: {
resolveDmPolicy: resolveGoogleChatDmPolicy,
dm: {
channelKey: "googlechat",
resolvePolicy: (account) => account.config.dm?.policy,
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
allowFromPathSuffix: "dm.",
normalizeEntry: (raw) => formatAllowFromEntry(raw),
},
collectWarnings: collectGoogleChatSecurityWarnings,
},
groups: {
resolveRequireMention: resolveGoogleChatGroupRequireMention,
},
threading: {
resolveReplyToMode: createTopLevelChannelReplyToModeResolver("googlechat"),
topLevelReplyToMode: "googlechat",
},
messaging: {
normalizeTarget: normalizeGoogleChatTarget,
targetResolver: {
looksLikeId: (raw, normalized) => {
const value = normalized ?? raw.trim();
return isGoogleChatSpaceTarget(value) || isGoogleChatUserTarget(value);
},
hint: "<spaces/{space}|users/{user}>",
},
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) =>
listResolvedDirectoryUserEntriesFromAllowFrom({
...params,
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry,
}),
listGroups: async (params) =>
listResolvedDirectoryGroupEntriesFromMapKeys({
...params,
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
resolveGroups: (account) => account.config.groups,
}),
}),
resolver: {
resolveTargets: async ({ inputs, kind }) => {
const resolved = inputs.map((input) => {
const normalized = normalizeGoogleChatTarget(input);
if (!normalized) {
return { input, resolved: false, note: "empty target" };
}
if (kind === "user" && isGoogleChatUserTarget(normalized)) {
return { input, resolved: true, id: normalized };
}
if (kind === "group" && isGoogleChatSpaceTarget(normalized)) {
return { input, resolved: true, id: normalized };
}
return {
input,
resolved: false,
note: "use spaces/{space} or users/{user}",
};
});
return resolved;
},
},
actions: googlechatActions,
outbound: {
deliveryMode: "direct",
chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
resolveTarget: ({ to }) => {
const trimmed = to?.trim() ?? "";
base: {
deliveryMode: "direct",
chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
resolveTarget: ({ to }) => {
const trimmed = to?.trim() ?? "";
if (trimmed) {
const normalized = normalizeGoogleChatTarget(trimmed);
if (!normalized) {
return {
ok: false,
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
};
if (trimmed) {
const normalized = normalizeGoogleChatTarget(trimmed);
if (!normalized) {
return {
ok: false,
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
};
}
return { ok: true, to: normalized };
}
return { ok: true, to: normalized };
}
return {
ok: false,
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
};
return {
ok: false,
error: missingTargetError("Google Chat", "<spaces/{space}|users/{user}>"),
};
},
},
...createAttachedChannelResultAdapter({
attachedResults: {
channel: "googlechat",
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
const account = resolveGoogleChatAccount({
@@ -356,114 +462,6 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
chatId: space,
};
},
}),
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
accounts.flatMap((entry) => {
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
const enabled = entry.enabled !== false;
const configured = entry.configured === true;
if (!enabled || !configured) {
return [];
}
const issues: ChannelStatusIssue[] = [];
if (!entry.audience) {
issues.push({
channel: "googlechat",
accountId,
kind: "config",
message: "Google Chat audience is missing (set channels.googlechat.audience).",
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
});
}
if (!entry.audienceType) {
issues.push({
channel: "googlechat",
accountId,
kind: "config",
message: "Google Chat audienceType is missing (app-url or project-number).",
fix: "Set channels.googlechat.audienceType and channels.googlechat.audience.",
});
}
return issues;
}),
buildChannelSummary: ({ snapshot }) =>
buildPassiveProbedChannelStatusSummary(snapshot, {
credentialSource: snapshot.credentialSource ?? "none",
audienceType: snapshot.audienceType ?? null,
audience: snapshot.audience ?? null,
webhookPath: snapshot.webhookPath ?? null,
webhookUrl: snapshot.webhookUrl ?? null,
}),
probeAccount: async ({ account }) =>
(await loadGoogleChatChannelRuntime()).probeGoogleChat(account),
buildAccountSnapshot: ({ account, runtime, probe }) => {
const base = buildComputedAccountStatusSnapshot({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
runtime,
probe,
});
return {
...base,
credentialSource: account.credentialSource,
audienceType: account.config.audienceType,
audience: account.config.audience,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
dmPolicy: account.config.dm?.policy ?? "pairing",
};
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const statusSink = createAccountStatusSink({
accountId: account.accountId,
setStatus: ctx.setStatus,
});
ctx.log?.info(`[${account.accountId}] starting Google Chat webhook`);
const { resolveGoogleChatWebhookPath, startGoogleChatMonitor } =
await loadGoogleChatChannelRuntime();
statusSink({
running: true,
lastStartAt: Date.now(),
webhookPath: resolveGoogleChatWebhookPath({ account }),
audienceType: account.config.audienceType,
audience: account.config.audience,
});
await runPassiveAccountLifecycle({
abortSignal: ctx.abortSignal,
start: async () =>
await startGoogleChatMonitor({
account,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
statusSink,
}),
stop: async (unregister) => {
unregister?.();
},
onStop: async () => {
statusSink({
running: false,
lastStopAt: Date.now(),
});
},
});
},
},
};
});

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildHuggingfaceProvider } from "./provider-catalog.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolFactory } from "./runtime-api.js";
import { createLobsterTool } from "./src/lobster-tool.js";

View File

@@ -1,5 +1,5 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/memory-core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export const buildPromptSection: MemoryPromptSectionBuilder = ({
availableTools,

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildMicrosoftSpeechProvider } from "openclaw/plugin-sdk/speech";
export default definePluginEntry({

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import {

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { buildNvidiaProvider } from "./provider-catalog.js";

View File

@@ -5,7 +5,7 @@ import {
type ProviderAuthMethodNonInteractiveContext,
type ProviderAuthResult,
type ProviderDiscoveryContext,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/plugin-entry";
import { OLLAMA_DEFAULT_BASE_URL, resolveOllamaApiBase } from "openclaw/plugin-sdk/provider-models";
const PROVIDER_ID = "ollama";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "openclaw/plugin-sdk/provider-models";
import { applyOpencodeGoConfig } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "openclaw/plugin-sdk/provider-models";
import { applyOpencodeZenConfig } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPerplexityWebSearchProvider } from "./src/perplexity-web-search-provider.js";
export default definePluginEntry({

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js";

View File

@@ -8,7 +8,7 @@ import {
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthMethodNonInteractiveContext,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/plugin-entry";
const PROVIDER_ID = "sglang";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
import { createTavilyExtractTool } from "./src/tavily-extract-tool.js";
import { createTavilyWebSearchProvider } from "./src/tavily-search-provider.js";
import { createTavilySearchTool } from "./src/tavily-search-tool.js";

View File

@@ -2,17 +2,10 @@ import {
buildDmGroupAccountAllowlistAdapter,
createNestedAllowlistOverrideResolver,
} from "openclaw/plugin-sdk/allowlist-config-edit";
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import {
createPairingPrefixStripper,
createTextPairingAdapter,
} from "openclaw/plugin-sdk/channel-pairing";
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import {
attachChannelToResult,
createAttachedChannelResultAdapter,
} from "openclaw/plugin-sdk/channel-send-result";
import { createTopLevelChannelReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { resolveExecApprovalCommandDisplay } from "openclaw/plugin-sdk/infra-runtime";
import { buildExecApprovalPendingReplyPayload } from "openclaw/plugin-sdk/infra-runtime";
@@ -32,7 +25,6 @@ import {
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
type ChannelPlugin,
type ChannelMessageActionAdapter,
type OpenClawConfig,
} from "../runtime-api.js";
@@ -279,14 +271,6 @@ const telegramMessageActions: ChannelMessageActionAdapter = {
},
};
const resolveTelegramDmPolicy = createScopedDmSecurityResolver<ResolvedTelegramAccount>({
channelKey: "telegram",
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""),
});
const resolveTelegramAllowlistGroupOverrides = createNestedAllowlistOverrideResolver({
resolveRecord: (account: ResolvedTelegramAccount) => account.config.groups,
outerLabel: (groupId) => groupId,
@@ -317,214 +301,440 @@ const collectTelegramSecurityWarnings =
},
});
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProbe> = {
...createTelegramPluginBase({
setupWizard: telegramSetupWizard,
setup: telegramSetupAdapter,
}),
pairing: createTextPairingAdapter({
idLabel: "telegramUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(telegram|tg):/i),
notify: async ({ cfg, id, message }) => {
const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg);
if (!token) {
throw new Error("telegram token not configured");
}
await getTelegramRuntime().channel.telegram.sendMessageTelegram(id, message, {
token,
});
export const telegramPlugin = createChatChannelPlugin({
base: {
...createTelegramPluginBase({
setupWizard: telegramSetupWizard,
setup: telegramSetupAdapter,
}),
allowlist: buildDmGroupAccountAllowlistAdapter({
channelId: "telegram",
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
normalize: ({ cfg, accountId, values }) =>
telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveGroupOverrides: resolveTelegramAllowlistGroupOverrides,
}),
bindings: {
compileConfiguredBinding: ({ conversationId }) =>
normalizeTelegramAcpConversationId(conversationId),
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) =>
matchTelegramAcpConversation({
bindingConversationId: compiledBinding.conversationId,
conversationId,
parentConversationId,
}),
},
groups: {
resolveRequireMention: resolveTelegramGroupRequireMention,
resolveToolPolicy: resolveTelegramGroupToolPolicy,
},
messaging: {
normalizeTarget: normalizeTelegramMessagingTarget,
parseExplicitTarget: ({ raw }) => parseTelegramExplicitTarget(raw),
inferTargetChatType: ({ to }) => parseTelegramExplicitTarget(to).chatType,
formatTargetDisplay: ({ target, display, kind }) => {
const formatted = display?.trim();
if (formatted) {
return formatted;
}
const trimmedTarget = target.trim();
if (!trimmedTarget) {
return trimmedTarget;
}
const withoutProvider = trimmedTarget.replace(/^(telegram|tg):/i, "");
if (kind === "user" || /^user:/i.test(withoutProvider)) {
return `@${withoutProvider.replace(/^user:/i, "")}`;
}
if (/^channel:/i.test(withoutProvider)) {
return `#${withoutProvider.replace(/^channel:/i, "")}`;
}
return withoutProvider;
},
resolveOutboundSessionRoute: (params) => resolveTelegramOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeTelegramTargetId,
hint: "<chatId>",
},
},
lifecycle: {
onAccountConfigChanged: async ({ prevCfg, nextCfg, accountId }) => {
const previousToken = resolveTelegramAccount({ cfg: prevCfg, accountId }).token.trim();
const nextToken = resolveTelegramAccount({ cfg: nextCfg, accountId }).token.trim();
if (previousToken !== nextToken) {
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
await deleteTelegramUpdateOffset({ accountId });
}
},
onAccountRemoved: async ({ accountId }) => {
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
await deleteTelegramUpdateOffset({ accountId });
},
},
execApprovals: {
getInitiatingSurfaceState: ({ cfg, accountId }) =>
isTelegramExecApprovalClientEnabled({ cfg, accountId })
? { kind: "enabled" }
: { kind: "disabled" },
hasConfiguredDmRoute: ({ cfg }) => hasTelegramExecApprovalDmRoute(cfg),
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
if (channel !== "telegram") {
return false;
}
const requestChannel = normalizeMessageChannel(request.request.turnSourceChannel ?? "");
if (requestChannel !== "telegram") {
return false;
}
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
},
buildPendingPayload: ({ request, nowMs }) => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: request.id,
approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id,
command: resolveExecApprovalCommandDisplay(request.request).commandText,
cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined,
expiresAtMs: request.expiresAtMs,
nowMs,
});
const buttons = buildTelegramExecApprovalButtons(request.id);
if (!buttons) {
return payload;
}
return {
...payload,
channelData: {
...payload.channelData,
telegram: {
buttons,
},
},
};
},
beforeDeliverPending: async ({ cfg, target, payload }) => {
const hasExecApprovalData =
payload.channelData &&
typeof payload.channelData === "object" &&
!Array.isArray(payload.channelData) &&
payload.channelData.execApproval;
if (!hasExecApprovalData) {
return;
}
const threadId =
typeof target.threadId === "number"
? target.threadId
: typeof target.threadId === "string"
? Number.parseInt(target.threadId, 10)
: undefined;
await sendTypingTelegram(target.to, {
cfg,
accountId: target.accountId ?? undefined,
...(Number.isFinite(threadId) ? { messageThreadId: threadId } : {}),
}).catch(() => {});
},
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) => listTelegramDirectoryPeersFromConfig(params),
listGroups: async (params) => listTelegramDirectoryGroupsFromConfig(params),
}),
actions: telegramMessageActions,
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: collectTelegramStatusIssues,
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
probeAccount: async ({ account, timeoutMs }) =>
probeTelegram(account.token, timeoutMs, {
accountId: account.accountId,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
}),
formatCapabilitiesProbe: ({ probe }) => {
const lines = [];
if (probe?.bot?.username) {
const botId = probe.bot.id ? ` (${probe.bot.id})` : "";
lines.push({ text: `Bot: @${probe.bot.username}${botId}` });
}
const flags: string[] = [];
if (typeof probe?.bot?.canJoinGroups === "boolean") {
flags.push(`joinGroups=${probe.bot.canJoinGroups}`);
}
if (typeof probe?.bot?.canReadAllGroupMessages === "boolean") {
flags.push(`readAllGroupMessages=${probe.bot.canReadAllGroupMessages}`);
}
if (typeof probe?.bot?.supportsInlineQueries === "boolean") {
flags.push(`inlineQueries=${probe.bot.supportsInlineQueries}`);
}
if (flags.length > 0) {
lines.push({ text: `Flags: ${flags.join(" ")}` });
}
if (probe?.webhook?.url !== undefined) {
lines.push({ text: `Webhook: ${probe.webhook.url || "none"}` });
}
return lines;
},
auditAccount: async ({ account, timeoutMs, probe, cfg }) => {
const groups =
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
cfg.channels?.telegram?.groups;
const { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups } =
collectTelegramUnmentionedGroupIds(groups);
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
return undefined;
}
const botId = probe?.ok && probe.bot?.id != null ? probe.bot.id : null;
if (!botId) {
return {
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
checkedGroups: 0,
unresolvedGroups,
hasWildcardUnmentionedGroups,
groups: [],
elapsedMs: 0,
};
}
const audit = await auditTelegramGroupMembership({
token: account.token,
botId,
groupIds,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
timeoutMs,
});
return { ...audit, unresolvedGroups, hasWildcardUnmentionedGroups };
},
buildAccountSnapshot: ({ account, cfg, runtime, probe, audit }) => {
const configuredFromStatus = resolveConfiguredFromCredentialStatuses(account);
const ownerAccountId = findTelegramTokenOwnerAccountId({
cfg,
accountId: account.accountId,
});
const duplicateTokenReason = ownerAccountId
? formatDuplicateTelegramTokenReason({
accountId: account.accountId,
ownerAccountId,
})
: null;
const configured =
(configuredFromStatus ?? Boolean(account.token?.trim())) && !ownerAccountId;
const groups =
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
cfg.channels?.telegram?.groups;
const allowUnmentionedGroups =
groups?.["*"]?.requireMention === false ||
Object.entries(groups ?? {}).some(
([key, value]) => key !== "*" && value?.requireMention === false,
);
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
...projectCredentialSnapshotFields(account),
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? duplicateTokenReason,
mode: runtime?.mode ?? (account.config.webhookUrl ? "webhook" : "polling"),
probe,
audit,
allowUnmentionedGroups,
lastInboundAt: runtime?.lastInboundAt ?? null,
lastOutboundAt: runtime?.lastOutboundAt ?? null,
};
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const ownerAccountId = findTelegramTokenOwnerAccountId({
cfg: ctx.cfg,
accountId: account.accountId,
});
if (ownerAccountId) {
const reason = formatDuplicateTelegramTokenReason({
accountId: account.accountId,
ownerAccountId,
});
ctx.log?.error?.(`[${account.accountId}] ${reason}`);
throw new Error(reason);
}
const token = (account.token ?? "").trim();
let telegramBotLabel = "";
try {
const probe = await probeTelegram(token, 2500, {
accountId: account.accountId,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
});
const username = probe.ok ? probe.bot?.username?.trim() : null;
if (username) {
telegramBotLabel = ` (@${username})`;
}
} catch (err) {
if (getTelegramRuntime().logging.shouldLogVerbose()) {
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
}
}
ctx.log?.info(`[${account.accountId}] starting provider${telegramBotLabel}`);
return monitorTelegramProvider({
token,
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
useWebhook: Boolean(account.config.webhookUrl),
webhookUrl: account.config.webhookUrl,
webhookSecret: account.config.webhookSecret,
webhookPath: account.config.webhookPath,
webhookHost: account.config.webhookHost,
webhookPort: account.config.webhookPort,
webhookCertPath: account.config.webhookCertPath,
});
},
logoutAccount: async ({ accountId, cfg }) => {
const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
const nextCfg = { ...cfg } as OpenClawConfig;
const nextTelegram = cfg.channels?.telegram ? { ...cfg.channels.telegram } : undefined;
let cleared = false;
let changed = false;
if (nextTelegram) {
if (accountId === DEFAULT_ACCOUNT_ID && nextTelegram.botToken) {
delete nextTelegram.botToken;
cleared = true;
changed = true;
}
const accountCleanup = clearAccountEntryFields({
accounts: nextTelegram.accounts,
accountId,
fields: ["botToken"],
});
if (accountCleanup.changed) {
changed = true;
if (accountCleanup.cleared) {
cleared = true;
}
if (accountCleanup.nextAccounts) {
nextTelegram.accounts = accountCleanup.nextAccounts;
} else {
delete nextTelegram.accounts;
}
}
}
if (changed) {
if (nextTelegram && Object.keys(nextTelegram).length > 0) {
nextCfg.channels = { ...nextCfg.channels, telegram: nextTelegram };
} else {
const nextChannels = { ...nextCfg.channels };
delete nextChannels.telegram;
if (Object.keys(nextChannels).length > 0) {
nextCfg.channels = nextChannels;
} else {
delete nextCfg.channels;
}
}
}
const resolved = resolveTelegramAccount({
cfg: changed ? nextCfg : cfg,
accountId,
});
const loggedOut = resolved.tokenSource === "none";
if (changed) {
await getTelegramRuntime().config.writeConfigFile(nextCfg);
}
return { cleared, envToken: Boolean(envToken), loggedOut };
},
},
},
pairing: {
text: {
idLabel: "telegramUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(telegram|tg):/i),
notify: async ({ cfg, id, message }) => {
const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg);
if (!token) {
throw new Error("telegram token not configured");
}
await getTelegramRuntime().channel.telegram.sendMessageTelegram(id, message, {
token,
});
},
},
}),
allowlist: buildDmGroupAccountAllowlistAdapter({
channelId: "telegram",
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
normalize: ({ cfg, accountId, values }) =>
telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveGroupOverrides: resolveTelegramAllowlistGroupOverrides,
}),
bindings: {
compileConfiguredBinding: ({ conversationId }) =>
normalizeTelegramAcpConversationId(conversationId),
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) =>
matchTelegramAcpConversation({
bindingConversationId: compiledBinding.conversationId,
conversationId,
parentConversationId,
}),
},
security: {
resolveDmPolicy: resolveTelegramDmPolicy,
dm: {
channelKey: "telegram",
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""),
},
collectWarnings: collectTelegramSecurityWarnings,
},
groups: {
resolveRequireMention: resolveTelegramGroupRequireMention,
resolveToolPolicy: resolveTelegramGroupToolPolicy,
},
threading: {
resolveReplyToMode: createTopLevelChannelReplyToModeResolver("telegram"),
topLevelReplyToMode: "telegram",
resolveAutoThreadId: ({ to, toolContext, replyToId }) =>
replyToId ? undefined : resolveTelegramAutoThreadId({ to, toolContext }),
},
messaging: {
normalizeTarget: normalizeTelegramMessagingTarget,
parseExplicitTarget: ({ raw }) => parseTelegramExplicitTarget(raw),
inferTargetChatType: ({ to }) => parseTelegramExplicitTarget(to).chatType,
formatTargetDisplay: ({ target, display, kind }) => {
const formatted = display?.trim();
if (formatted) {
return formatted;
}
const trimmedTarget = target.trim();
if (!trimmedTarget) {
return trimmedTarget;
}
const withoutProvider = trimmedTarget.replace(/^(telegram|tg):/i, "");
if (kind === "user" || /^user:/i.test(withoutProvider)) {
return `@${withoutProvider.replace(/^user:/i, "")}`;
}
if (/^channel:/i.test(withoutProvider)) {
return `#${withoutProvider.replace(/^channel:/i, "")}`;
}
return withoutProvider;
},
resolveOutboundSessionRoute: (params) => resolveTelegramOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeTelegramTargetId,
hint: "<chatId>",
},
},
lifecycle: {
onAccountConfigChanged: async ({ prevCfg, nextCfg, accountId }) => {
const previousToken = resolveTelegramAccount({ cfg: prevCfg, accountId }).token.trim();
const nextToken = resolveTelegramAccount({ cfg: nextCfg, accountId }).token.trim();
if (previousToken !== nextToken) {
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
await deleteTelegramUpdateOffset({ accountId });
}
},
onAccountRemoved: async ({ accountId }) => {
const { deleteTelegramUpdateOffset } = await import("./update-offset-store.js");
await deleteTelegramUpdateOffset({ accountId });
},
},
execApprovals: {
getInitiatingSurfaceState: ({ cfg, accountId }) =>
isTelegramExecApprovalClientEnabled({ cfg, accountId })
? { kind: "enabled" }
: { kind: "disabled" },
hasConfiguredDmRoute: ({ cfg }) => hasTelegramExecApprovalDmRoute(cfg),
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
if (channel !== "telegram") {
return false;
}
const requestChannel = normalizeMessageChannel(request.request.turnSourceChannel ?? "");
if (requestChannel !== "telegram") {
return false;
}
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
},
buildPendingPayload: ({ request, nowMs }) => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: request.id,
approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id,
command: resolveExecApprovalCommandDisplay(request.request).commandText,
cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined,
expiresAtMs: request.expiresAtMs,
nowMs,
});
const buttons = buildTelegramExecApprovalButtons(request.id);
if (!buttons) {
return payload;
}
return {
...payload,
channelData: {
...payload.channelData,
telegram: {
buttons,
},
},
};
},
beforeDeliverPending: async ({ cfg, target, payload }) => {
const hasExecApprovalData =
payload.channelData &&
typeof payload.channelData === "object" &&
!Array.isArray(payload.channelData) &&
payload.channelData.execApproval;
if (!hasExecApprovalData) {
return;
}
const threadId =
typeof target.threadId === "number"
? target.threadId
: typeof target.threadId === "string"
? Number.parseInt(target.threadId, 10)
: undefined;
await sendTypingTelegram(target.to, {
cfg,
accountId: target.accountId ?? undefined,
...(Number.isFinite(threadId) ? { messageThreadId: threadId } : {}),
}).catch(() => {});
},
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) => listTelegramDirectoryPeersFromConfig(params),
listGroups: async (params) => listTelegramDirectoryGroupsFromConfig(params),
}),
actions: telegramMessageActions,
setup: telegramSetupAdapter,
outbound: {
deliveryMode: "direct",
chunker: (text, limit) => getTelegramRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
pollMaxOptions: 10,
shouldSkipPlainTextSanitization: ({ payload }) => Boolean(payload.channelData),
resolveEffectiveTextChunkLimit: ({ fallbackLimit }) =>
typeof fallbackLimit === "number" ? Math.min(fallbackLimit, 4096) : 4096,
sendPayload: async ({
cfg,
to,
payload,
mediaLocalRoots,
accountId,
deps,
replyToId,
threadId,
silent,
forceDocument,
}) => {
const send =
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
getTelegramRuntime().channel.telegram.sendMessageTelegram;
const result = await sendTelegramPayloadMessages({
send,
base: {
deliveryMode: "direct",
chunker: (text, limit) => getTelegramRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
pollMaxOptions: 10,
shouldSkipPlainTextSanitization: ({ payload }) => Boolean(payload.channelData),
resolveEffectiveTextChunkLimit: ({ fallbackLimit }) =>
typeof fallbackLimit === "number" ? Math.min(fallbackLimit, 4096) : 4096,
sendPayload: async ({
cfg,
to,
payload,
baseOpts: buildTelegramSendOptions({
cfg,
mediaLocalRoots,
accountId,
replyToId,
threadId,
silent,
forceDocument,
}),
});
return attachChannelToResult("telegram", result);
mediaLocalRoots,
accountId,
deps,
replyToId,
threadId,
silent,
forceDocument,
}) => {
const send =
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
getTelegramRuntime().channel.telegram.sendMessageTelegram;
const result = await sendTelegramPayloadMessages({
send,
to,
payload,
baseOpts: buildTelegramSendOptions({
cfg,
mediaLocalRoots,
accountId,
replyToId,
threadId,
silent,
forceDocument,
}),
});
return attachChannelToResult("telegram", result);
},
},
...createAttachedChannelResultAdapter({
attachedResults: {
channel: "telegram",
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) =>
await sendTelegramOutbound({
@@ -569,221 +779,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
silent: silent ?? undefined,
isAnonymous: isAnonymous ?? undefined,
}),
}),
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: collectTelegramStatusIssues,
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
probeAccount: async ({ account, timeoutMs }) =>
probeTelegram(account.token, timeoutMs, {
accountId: account.accountId,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
}),
formatCapabilitiesProbe: ({ probe }) => {
const lines = [];
if (probe?.bot?.username) {
const botId = probe.bot.id ? ` (${probe.bot.id})` : "";
lines.push({ text: `Bot: @${probe.bot.username}${botId}` });
}
const flags: string[] = [];
if (typeof probe?.bot?.canJoinGroups === "boolean") {
flags.push(`joinGroups=${probe.bot.canJoinGroups}`);
}
if (typeof probe?.bot?.canReadAllGroupMessages === "boolean") {
flags.push(`readAllGroupMessages=${probe.bot.canReadAllGroupMessages}`);
}
if (typeof probe?.bot?.supportsInlineQueries === "boolean") {
flags.push(`inlineQueries=${probe.bot.supportsInlineQueries}`);
}
if (flags.length > 0) {
lines.push({ text: `Flags: ${flags.join(" ")}` });
}
if (probe?.webhook?.url !== undefined) {
lines.push({ text: `Webhook: ${probe.webhook.url || "none"}` });
}
return lines;
},
auditAccount: async ({ account, timeoutMs, probe, cfg }) => {
const groups =
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
cfg.channels?.telegram?.groups;
const { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups } =
collectTelegramUnmentionedGroupIds(groups);
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
return undefined;
}
const botId = probe?.ok && probe.bot?.id != null ? probe.bot.id : null;
if (!botId) {
return {
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
checkedGroups: 0,
unresolvedGroups,
hasWildcardUnmentionedGroups,
groups: [],
elapsedMs: 0,
};
}
const audit = await auditTelegramGroupMembership({
token: account.token,
botId,
groupIds,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
timeoutMs,
});
return { ...audit, unresolvedGroups, hasWildcardUnmentionedGroups };
},
buildAccountSnapshot: ({ account, cfg, runtime, probe, audit }) => {
const configuredFromStatus = resolveConfiguredFromCredentialStatuses(account);
const ownerAccountId = findTelegramTokenOwnerAccountId({
cfg,
accountId: account.accountId,
});
const duplicateTokenReason = ownerAccountId
? formatDuplicateTelegramTokenReason({
accountId: account.accountId,
ownerAccountId,
})
: null;
const configured =
(configuredFromStatus ?? Boolean(account.token?.trim())) && !ownerAccountId;
const groups =
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
cfg.channels?.telegram?.groups;
const allowUnmentionedGroups =
groups?.["*"]?.requireMention === false ||
Object.entries(groups ?? {}).some(
([key, value]) => key !== "*" && value?.requireMention === false,
);
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
...projectCredentialSnapshotFields(account),
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? duplicateTokenReason,
mode: runtime?.mode ?? (account.config.webhookUrl ? "webhook" : "polling"),
probe,
audit,
allowUnmentionedGroups,
lastInboundAt: runtime?.lastInboundAt ?? null,
lastOutboundAt: runtime?.lastOutboundAt ?? null,
};
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const ownerAccountId = findTelegramTokenOwnerAccountId({
cfg: ctx.cfg,
accountId: account.accountId,
});
if (ownerAccountId) {
const reason = formatDuplicateTelegramTokenReason({
accountId: account.accountId,
ownerAccountId,
});
ctx.log?.error?.(`[${account.accountId}] ${reason}`);
throw new Error(reason);
}
const token = (account.token ?? "").trim();
let telegramBotLabel = "";
try {
const probe = await probeTelegram(token, 2500, {
accountId: account.accountId,
proxyUrl: account.config.proxy,
network: account.config.network,
apiRoot: account.config.apiRoot,
});
const username = probe.ok ? probe.bot?.username?.trim() : null;
if (username) {
telegramBotLabel = ` (@${username})`;
}
} catch (err) {
if (getTelegramRuntime().logging.shouldLogVerbose()) {
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
}
}
ctx.log?.info(`[${account.accountId}] starting provider${telegramBotLabel}`);
return monitorTelegramProvider({
token,
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
useWebhook: Boolean(account.config.webhookUrl),
webhookUrl: account.config.webhookUrl,
webhookSecret: account.config.webhookSecret,
webhookPath: account.config.webhookPath,
webhookHost: account.config.webhookHost,
webhookPort: account.config.webhookPort,
webhookCertPath: account.config.webhookCertPath,
});
},
logoutAccount: async ({ accountId, cfg }) => {
const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
const nextCfg = { ...cfg } as OpenClawConfig;
const nextTelegram = cfg.channels?.telegram ? { ...cfg.channels.telegram } : undefined;
let cleared = false;
let changed = false;
if (nextTelegram) {
if (accountId === DEFAULT_ACCOUNT_ID && nextTelegram.botToken) {
delete nextTelegram.botToken;
cleared = true;
changed = true;
}
const accountCleanup = clearAccountEntryFields({
accounts: nextTelegram.accounts,
accountId,
fields: ["botToken"],
});
if (accountCleanup.changed) {
changed = true;
if (accountCleanup.cleared) {
cleared = true;
}
if (accountCleanup.nextAccounts) {
nextTelegram.accounts = accountCleanup.nextAccounts;
} else {
delete nextTelegram.accounts;
}
}
}
if (changed) {
if (nextTelegram && Object.keys(nextTelegram).length > 0) {
nextCfg.channels = { ...nextCfg.channels, telegram: nextTelegram };
} else {
const nextChannels = { ...nextCfg.channels };
delete nextChannels.telegram;
if (Object.keys(nextChannels).length > 0) {
nextCfg.channels = nextChannels;
} else {
delete nextCfg.channels;
}
}
}
const resolved = resolveTelegramAccount({
cfg: changed ? nextCfg : cfg,
accountId,
});
const loggedOut = resolved.tokenSource === "none";
if (changed) {
await getTelegramRuntime().config.writeConfigFile(nextCfg);
}
return { cleared, envToken: Boolean(envToken), loggedOut };
},
},
};
});

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyTogetherConfig, TOGETHER_DEFAULT_MODEL_REF } from "./onboard.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-models";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "./onboard.js";

View File

@@ -8,7 +8,7 @@ import {
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthMethodNonInteractiveContext,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/plugin-entry";
const PROVIDER_ID = "vllm";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
import { buildDoubaoCodingProvider, buildDoubaoProvider } from "./provider-catalog.js";

View File

@@ -1,4 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { PROVIDER_LABELS } from "openclaw/plugin-sdk/provider-usage";

View File

@@ -5,7 +5,7 @@ import {
type ProviderAuthMethodNonInteractiveContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
buildApiKeyCredential,

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled copilot-proxy plugin.
// Keep this list additive and scoped to symbols used under extensions/copilot-proxy.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type {
OpenClawPluginApi,
ProviderAuthContext,

View File

@@ -1,21 +1,29 @@
import {
createScopedAccountReplyToModeResolver,
createTopLevelChannelReplyToModeResolver,
} from "../channels/plugins/threading-helpers.js";
import type {
ChannelOutboundAdapter,
ChannelPairingAdapter,
ChannelSecurityAdapter,
} from "../channels/plugins/types.adapters.js";
import type {
ChannelMessagingAdapter,
ChannelOutboundSessionRoute,
ChannelThreadingAdapter,
} from "../channels/plugins/types.core.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import { getChatChannelMeta } from "../channels/registry.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ReplyToMode } from "../config/types.base.js";
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,
OpenClawPluginConfigSchema,
OpenClawPluginDefinition,
PluginCommandContext,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
import { createScopedDmSecurityResolver } from "./channel-config-helpers.js";
import { createTextPairingAdapter } from "./channel-pairing.js";
import { createAttachedChannelResultAdapter } from "./channel-send-result.js";
import { definePluginEntry } from "./plugin-entry.js";
export type {
AnyAgentTool,
@@ -77,6 +85,7 @@ export type { OpenClawPluginApi } from "../plugins/types.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { definePluginEntry } from "./plugin-entry.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
@@ -177,28 +186,11 @@ type DefineChannelPluginEntryOptions<TPlugin extends ChannelPlugin = ChannelPlug
name: string;
description: string;
plugin: TPlugin;
configSchema?: DefinePluginEntryOptions["configSchema"];
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
setRuntime?: (runtime: PluginRuntime) => void;
registerFull?: (api: OpenClawPluginApi) => void;
};
type DefinePluginEntryOptions = {
id: string;
name: string;
description: string;
kind?: OpenClawPluginDefinition["kind"];
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
register: (api: OpenClawPluginApi) => void;
};
type DefinedPluginEntry = {
id: string;
name: string;
description: string;
configSchema: OpenClawPluginConfigSchema;
register: NonNullable<OpenClawPluginDefinition["register"]>;
} & Pick<OpenClawPluginDefinition, "kind">;
type CreateChannelPluginBaseOptions<TResolvedAccount> = {
id: ChannelPlugin<TResolvedAccount>["id"];
meta?: Partial<NonNullable<ChannelPlugin<TResolvedAccount>["meta"]>>;
@@ -235,31 +227,6 @@ type CreatedChannelPluginBase<TResolvedAccount> = Pick<
>
>;
function resolvePluginConfigSchema(
configSchema: DefinePluginEntryOptions["configSchema"] = emptyPluginConfigSchema,
): OpenClawPluginConfigSchema {
return typeof configSchema === "function" ? configSchema() : configSchema;
}
// Shared generic plugin-entry boilerplate for bundled and third-party plugins.
export function definePluginEntry({
id,
name,
description,
kind,
configSchema = emptyPluginConfigSchema,
register,
}: DefinePluginEntryOptions): DefinedPluginEntry {
return {
id,
name,
description,
...(kind ? { kind } : {}),
configSchema: resolvePluginConfigSchema(configSchema),
register,
};
}
// Shared channel-plugin entry boilerplate for bundled and third-party channels.
export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
id,
@@ -291,6 +258,161 @@ export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin) {
return { plugin };
}
type ChatChannelPluginBase<TResolvedAccount, Probe, Audit> = Omit<
ChannelPlugin<TResolvedAccount, Probe, Audit>,
"security" | "pairing" | "threading" | "outbound"
> &
Partial<
Pick<
ChannelPlugin<TResolvedAccount, Probe, Audit>,
"security" | "pairing" | "threading" | "outbound"
>
>;
type ChatChannelSecurityOptions<TResolvedAccount extends { accountId?: string | null }> = {
dm: {
channelKey: string;
resolvePolicy: (account: TResolvedAccount) => string | null | undefined;
resolveAllowFrom: (account: TResolvedAccount) => Array<string | number> | null | undefined;
resolveFallbackAccountId?: (account: TResolvedAccount) => string | null | undefined;
defaultPolicy?: string;
allowFromPathSuffix?: string;
policyPathSuffix?: string;
approveChannelId?: string;
approveHint?: string;
normalizeEntry?: (raw: string) => string;
};
collectWarnings?: ChannelSecurityAdapter<TResolvedAccount>["collectWarnings"];
};
type ChatChannelPairingOptions = {
text: {
idLabel: string;
message: string;
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
notify: Parameters<typeof createTextPairingAdapter>[0]["notify"];
};
};
type ChatChannelThreadingReplyModeOptions<TResolvedAccount> =
| { topLevelReplyToMode: string }
| {
scopedAccountReplyToMode: {
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => TResolvedAccount;
resolveReplyToMode: (
account: TResolvedAccount,
chatType?: string | null,
) => ReplyToMode | null | undefined;
fallback?: ReplyToMode;
};
}
| {
resolveReplyToMode: NonNullable<ChannelThreadingAdapter["resolveReplyToMode"]>;
};
type ChatChannelThreadingOptions<TResolvedAccount> =
ChatChannelThreadingReplyModeOptions<TResolvedAccount> &
Omit<ChannelThreadingAdapter, "resolveReplyToMode">;
type ChatChannelAttachedOutboundOptions = {
base: Omit<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
attachedResults: Parameters<typeof createAttachedChannelResultAdapter>[0];
};
function resolveChatChannelSecurity<TResolvedAccount extends { accountId?: string | null }>(
security:
| ChannelSecurityAdapter<TResolvedAccount>
| ChatChannelSecurityOptions<TResolvedAccount>
| undefined,
): ChannelSecurityAdapter<TResolvedAccount> | undefined {
if (!security) {
return undefined;
}
if (!("dm" in security)) {
return security;
}
return {
resolveDmPolicy: createScopedDmSecurityResolver<TResolvedAccount>(security.dm),
...(security.collectWarnings ? { collectWarnings: security.collectWarnings } : {}),
};
}
function resolveChatChannelPairing(
pairing: ChannelPairingAdapter | ChatChannelPairingOptions | undefined,
): ChannelPairingAdapter | undefined {
if (!pairing) {
return undefined;
}
if (!("text" in pairing)) {
return pairing;
}
return createTextPairingAdapter(pairing.text);
}
function resolveChatChannelThreading<TResolvedAccount>(
threading: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount> | undefined,
): ChannelThreadingAdapter | undefined {
if (!threading) {
return undefined;
}
if (!("topLevelReplyToMode" in threading) && !("scopedAccountReplyToMode" in threading)) {
return threading;
}
let resolveReplyToMode: ChannelThreadingAdapter["resolveReplyToMode"];
if ("topLevelReplyToMode" in threading) {
resolveReplyToMode = createTopLevelChannelReplyToModeResolver(threading.topLevelReplyToMode);
} else {
resolveReplyToMode = createScopedAccountReplyToModeResolver<TResolvedAccount>(
threading.scopedAccountReplyToMode,
);
}
return {
...threading,
resolveReplyToMode,
};
}
function resolveChatChannelOutbound(
outbound: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions | undefined,
): ChannelOutboundAdapter | undefined {
if (!outbound) {
return undefined;
}
if (!("attachedResults" in outbound)) {
return outbound;
}
return {
...outbound.base,
...createAttachedChannelResultAdapter(outbound.attachedResults),
};
}
// Shared higher-level builder for chat-style channels that mostly compose
// scoped DM security, text pairing, reply threading, and attached send results.
export function createChatChannelPlugin<
TResolvedAccount extends { accountId?: string | null },
Probe = unknown,
Audit = unknown,
>(params: {
base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>;
security?:
| ChannelSecurityAdapter<TResolvedAccount>
| ChatChannelSecurityOptions<TResolvedAccount>;
pairing?: ChannelPairingAdapter | ChatChannelPairingOptions;
threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>;
outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions;
}): ChannelPlugin<TResolvedAccount, Probe, Audit> {
return {
...params.base,
...(params.security ? { security: resolveChatChannelSecurity(params.security) } : {}),
...(params.pairing ? { pairing: resolveChatChannelPairing(params.pairing) } : {}),
...(params.threading ? { threading: resolveChatChannelThreading(params.threading) } : {}),
...(params.outbound ? { outbound: resolveChatChannelOutbound(params.outbound) } : {}),
} as ChannelPlugin<TResolvedAccount, Probe, Audit>;
}
// Shared base object for channel plugins that only need to override a few optional surfaces.
export function createChannelPluginBase<TResolvedAccount>(
params: CreateChannelPluginBaseOptions<TResolvedAccount>,

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled diffs plugin.
// Keep this list additive and scoped to symbols used under extensions/diffs.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawConfig } from "../config/config.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export type {

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled llm-task plugin.
// Keep this list additive and scoped to symbols used under extensions/llm-task.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export {
formatThinkingLevels,

View File

@@ -1,7 +1,7 @@
// Private Lobster plugin helpers for bundled extensions.
// Keep this surface narrow and limited to the Lobster workflow/tool contract.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export {
applyWindowsSpawnProgramPolicy,
materializeWindowsSpawnProgram,

View File

@@ -1,5 +1,5 @@
// Narrow plugin-sdk surface for the bundled memory-lancedb plugin.
// Keep this list additive and scoped to symbols used under extensions/memory-lancedb.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,5 +1,5 @@
// Narrow plugin-sdk surface for the bundled open-prose plugin.
// Keep this list additive and scoped to symbols used under extensions/open-prose.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled phone-control plugin.
// Keep this list additive and scoped to symbols used under extensions/phone-control.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,

View File

@@ -0,0 +1,33 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const EXTENSIONS_DIR = resolve(REPO_ROOT, "extensions");
const CORE_PLUGIN_ENTRY_IMPORT_RE =
/import\s*\{[^}]*\bdefinePluginEntry\b[^}]*\}\s*from\s*"openclaw\/plugin-sdk\/core"/;
describe("plugin entry guardrails", () => {
it("keeps bundled extension entry modules off direct definePluginEntry imports from core", () => {
const failures: string[] = [];
for (const entry of readdirSync(EXTENSIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const indexPath = resolve(EXTENSIONS_DIR, entry.name, "index.ts");
try {
const source = readFileSync(indexPath, "utf8");
if (CORE_PLUGIN_ENTRY_IMPORT_RE.test(source)) {
failures.push(`extensions/${entry.name}/index.ts`);
}
} catch {
// Skip extensions without index.ts entry modules.
}
}
expect(failures).toEqual([]);
});
});

View File

@@ -39,6 +39,7 @@ import * as lazyRuntimeSdk from "openclaw/plugin-sdk/lazy-runtime";
import * as matrixRuntimeSharedSdk from "openclaw/plugin-sdk/matrix-runtime-shared";
import * as mediaRuntimeSdk from "openclaw/plugin-sdk/media-runtime";
import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup";
import * as pluginEntrySdk from "openclaw/plugin-sdk/plugin-entry";
import * as providerAuthSdk from "openclaw/plugin-sdk/provider-auth";
import * as providerModelsSdk from "openclaw/plugin-sdk/provider-models";
import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup";
@@ -140,6 +141,7 @@ describe("plugin-sdk subpath exports", () => {
expect(typeof coreSdk.definePluginEntry).toBe("function");
expect(typeof coreSdk.defineChannelPluginEntry).toBe("function");
expect(typeof coreSdk.defineSetupPluginEntry).toBe("function");
expect(typeof coreSdk.createChatChannelPlugin).toBe("function");
expect(typeof coreSdk.createChannelPluginBase).toBe("function");
expect(typeof coreSdk.isSecretRef).toBe("function");
expect(typeof coreSdk.optionalStringEnum).toBe("function");
@@ -148,6 +150,10 @@ describe("plugin-sdk subpath exports", () => {
expect("registerSandboxBackend" in asExports(coreSdk)).toBe(false);
});
it("re-exports the canonical plugin entry helper from core", () => {
expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry);
});
it("exports routing helpers from the dedicated subpath", () => {
expect(typeof routingSdk.buildAgentSessionKey).toBe("function");
expect(typeof routingSdk.resolveThreadSessionKeys).toBe("function");

View File

@@ -1,6 +1,6 @@
// Narrow plugin-sdk surface for the bundled thread-ownership plugin.
// Keep this list additive and scoped to symbols used under extensions/thread-ownership.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawConfig } from "../config/config.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,7 +1,7 @@
// Private helper surface for the bundled voice-call plugin.
// Keep this surface narrow and limited to the voice-call feature contract.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export {
TtsAutoSchema,
TtsConfigSchema,