mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
refactor: tighten plugin sdk entry surface
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
buildApiKeyCredential,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
33
src/plugin-sdk/plugin-entry-guardrails.test.ts
Normal file
33
src/plugin-sdk/plugin-entry-guardrails.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user