mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
refactor: register channel bootstrap capabilities
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
type ResolvedBlueBubblesAccount,
|
||||
@@ -35,17 +35,18 @@ import {
|
||||
} from "./channel-shared.js";
|
||||
import type { BlueBubblesProbe } from "./channel.runtime.js";
|
||||
import { createBlueBubblesConversationBindingManager } from "./conversation-bindings.js";
|
||||
import { bluebubblesDoctor } from "./doctor.js";
|
||||
import {
|
||||
matchBlueBubblesAcpConversation,
|
||||
normalizeBlueBubblesAcpConversationId,
|
||||
resolveBlueBubblesConversationIdFromTarget,
|
||||
} from "./conversation-id.js";
|
||||
import { bluebubblesDoctor } from "./doctor.js";
|
||||
import {
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "./runtime-api.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { resolveBlueBubblesOutboundSessionRoute } from "./session-route.js";
|
||||
import { blueBubblesSetupAdapter } from "./setup-core.js";
|
||||
import { blueBubblesSetupWizard } from "./setup-surface.js";
|
||||
@@ -112,6 +113,10 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
}),
|
||||
},
|
||||
actions: bluebubblesMessageActions,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
bindings: {
|
||||
compileConfiguredBinding: ({ conversationId }) =>
|
||||
normalizeBlueBubblesAcpConversationId(conversationId),
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-config-contract.js";
|
||||
export {
|
||||
unsupportedSecretRefSurfacePatterns,
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
} from "./src/security-contract.js";
|
||||
export { deriveLegacySessionChatType } from "./src/session-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.DISCORD_BOT_TOKEN === "string" &&
|
||||
params.env.DISCORD_BOT_TOKEN.trim().length > 0
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,15 @@ import {
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
|
||||
import { DiscordChannelConfigSchema } from "./config-schema.js";
|
||||
import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js";
|
||||
import {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./secret-config-contract.js";
|
||||
import {
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
unsupportedSecretRefSurfacePatterns,
|
||||
} from "./security-contract.js";
|
||||
import { deriveLegacySessionChatType } from "./session-contract.js";
|
||||
|
||||
export const DISCORD_CHANNEL = "discord" as const;
|
||||
|
||||
@@ -87,6 +96,8 @@ export function createDiscordPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
| "secrets"
|
||||
> {
|
||||
return {
|
||||
id: DISCORD_CHANNEL,
|
||||
@@ -114,6 +125,8 @@ export function createDiscordPluginBase(params: {
|
||||
configSchema: DiscordChannelConfigSchema,
|
||||
config: {
|
||||
...discordConfigAdapter,
|
||||
hasConfiguredState: ({ env }) =>
|
||||
typeof env?.DISCORD_BOT_TOKEN === "string" && env.DISCORD_BOT_TOKEN.trim().length > 0,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
@@ -124,6 +137,15 @@ export function createDiscordPluginBase(params: {
|
||||
},
|
||||
}),
|
||||
},
|
||||
messaging: {
|
||||
deriveLegacySessionChatType,
|
||||
},
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
unsupportedSecretRefSurfacePatterns,
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
setup: params.setup,
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedDiscordAccount>,
|
||||
@@ -138,5 +160,7 @@ export function createDiscordPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
| "secrets"
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export { messageActionTargetAliases } from "./src/message-action-contract.js";
|
||||
@@ -58,8 +58,10 @@ import {
|
||||
parseFeishuTargetId,
|
||||
} from "./conversation-id.js";
|
||||
import { listFeishuDirectoryPeers, listFeishuDirectoryGroups } from "./directory.static.js";
|
||||
import { messageActionTargetAliases } from "./message-action-contract.js";
|
||||
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { collectFeishuSecurityAuditFindings } from "./security-audit.js";
|
||||
import {
|
||||
resolveFeishuParentConversationCandidates,
|
||||
@@ -641,7 +643,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
}),
|
||||
},
|
||||
auth: feishuApprovalAuth,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
actions: {
|
||||
messageActionTargetAliases,
|
||||
describeMessageTool: describeFeishuMessageTool,
|
||||
handleAction: async (ctx) => {
|
||||
const account = resolveFeishuAccount({
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { collectGoogleChatMutableAllowlistWarnings } from "./doctor.js";
|
||||
import { resolveGoogleChatGroupRequireMention } from "./group-policy.js";
|
||||
import { getGoogleChatRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { googlechatSetupAdapter } from "./setup-core.js";
|
||||
import { googlechatSetupWizard } from "./setup-surface.js";
|
||||
|
||||
@@ -163,6 +164,10 @@ export const googlechatPlugin = createChatChannelPlugin({
|
||||
}),
|
||||
},
|
||||
auth: googleChatApprovalAuth,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveGoogleChatGroupRequireMention,
|
||||
},
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveIMessageAccount } from "./src/accounts.js";
|
||||
|
||||
const DEFAULT_IMESSAGE_ATTACHMENT_ROOTS = ["/Users/*/Library/Messages/Attachments"] as const;
|
||||
const WILDCARD_SEGMENT = "*";
|
||||
const WINDOWS_DRIVE_ABS_RE = /^[A-Za-z]:\//;
|
||||
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:$/;
|
||||
|
||||
function normalizePosixAbsolutePath(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed.includes("\0")) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = path.posix.normalize(trimmed.replaceAll("\\", "/"));
|
||||
const isAbsolute = normalized.startsWith("/") || WINDOWS_DRIVE_ABS_RE.test(normalized);
|
||||
if (!isAbsolute || normalized === "/") {
|
||||
return undefined;
|
||||
}
|
||||
const withoutTrailingSlash = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
||||
if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
|
||||
return undefined;
|
||||
}
|
||||
return withoutTrailingSlash;
|
||||
}
|
||||
|
||||
function splitPathSegments(value: string): string[] {
|
||||
return value.split("/").filter(Boolean);
|
||||
}
|
||||
|
||||
function isValidInboundPathRootPattern(value: string): boolean {
|
||||
const normalized = normalizePosixAbsolutePath(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const segments = splitPathSegments(normalized);
|
||||
if (segments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return segments.every((segment) => segment === WILDCARD_SEGMENT || !segment.includes("*"));
|
||||
}
|
||||
|
||||
function normalizeInboundPathRoots(roots?: readonly string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const root of roots ?? []) {
|
||||
if (typeof root !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (!isValidInboundPathRootPattern(root)) {
|
||||
continue;
|
||||
}
|
||||
const candidate = normalizePosixAbsolutePath(root);
|
||||
if (!candidate || seen.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(candidate);
|
||||
normalized.push(candidate);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mergeInboundPathRoots(...rootsLists: Array<readonly string[] | undefined>): string[] {
|
||||
const merged: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const roots of rootsLists) {
|
||||
const normalized = normalizeInboundPathRoots(roots);
|
||||
for (const root of normalized) {
|
||||
if (seen.has(root)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(root);
|
||||
merged.push(root);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function resolveInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): string[] {
|
||||
const account = resolveIMessageAccount(params);
|
||||
return mergeInboundPathRoots(
|
||||
account.config.attachmentRoots,
|
||||
params.cfg.channels?.imessage?.attachmentRoots,
|
||||
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRemoteInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): string[] {
|
||||
const account = resolveIMessageAccount(params);
|
||||
return mergeInboundPathRoots(
|
||||
account.config.remoteAttachmentRoots,
|
||||
params.cfg.channels?.imessage?.remoteAttachmentRoots,
|
||||
account.config.attachmentRoots,
|
||||
params.cfg.channels?.imessage?.attachmentRoots,
|
||||
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
} from "./accounts.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
|
||||
import { IMessageChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
resolveIMessageAttachmentRoots,
|
||||
resolveIMessageRemoteAttachmentRoots,
|
||||
} from "./media-contract.js";
|
||||
import { createIMessageSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const IMESSAGE_CHANNEL = "imessage" as const;
|
||||
@@ -65,8 +69,9 @@ export function createIMessagePluginBase(params: {
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
> {
|
||||
return createChannelPluginBase({
|
||||
const base = createChannelPluginBase({
|
||||
id: IMESSAGE_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(IMESSAGE_CHANNEL),
|
||||
@@ -91,7 +96,16 @@ export function createIMessagePluginBase(params: {
|
||||
},
|
||||
security: imessageSecurityAdapter,
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
messaging: {
|
||||
resolveInboundAttachmentRoots: (params) =>
|
||||
resolveIMessageAttachmentRoots({ accountId: params.accountId, cfg: params.cfg }),
|
||||
resolveRemoteInboundAttachmentRoots: (params) =>
|
||||
resolveIMessageRemoteAttachmentRoots({ accountId: params.accountId, cfg: params.cfg }),
|
||||
},
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedIMessageAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
@@ -102,5 +116,6 @@ export function createIMessagePluginBase(params: {
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.IRC_HOST === "string" &&
|
||||
params.env.IRC_HOST.trim().length > 0 &&
|
||||
typeof params.env?.IRC_NICK === "string" &&
|
||||
params.env.IRC_NICK.trim().length > 0
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js";
|
||||
import { probeIrc } from "./probe.js";
|
||||
import { getIrcRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { sendMessageIrc } from "./send.js";
|
||||
import { ircSetupAdapter } from "./setup-core.js";
|
||||
import { ircSetupWizard } from "./setup-surface.js";
|
||||
@@ -174,6 +175,11 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
|
||||
configSchema: IrcChannelConfigSchema,
|
||||
config: {
|
||||
...ircConfigAdapter,
|
||||
hasConfiguredState: ({ env }) =>
|
||||
typeof env?.IRC_HOST === "string" &&
|
||||
env.IRC_HOST.trim().length > 0 &&
|
||||
typeof env?.IRC_NICK === "string" &&
|
||||
env.IRC_NICK.trim().length > 0,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
@@ -188,6 +194,10 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
|
||||
},
|
||||
}),
|
||||
},
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
doctor: {
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
collectMutableAllowlistWarnings: collectIrcMutableAllowlistWarnings,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,10 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
namedAccountPromotionKeys,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
singleAccountKeysToMove,
|
||||
} from "./src/setup-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -62,7 +62,13 @@ import {
|
||||
setMatrixThreadBindingMaxAgeBySessionKey,
|
||||
} from "./matrix/thread-bindings-shared.js";
|
||||
import { getMatrixRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { resolveMatrixOutboundSessionRoute } from "./session-route.js";
|
||||
import {
|
||||
namedAccountPromotionKeys,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
singleAccountKeysToMove,
|
||||
} from "./setup-contract.js";
|
||||
import { matrixSetupAdapter } from "./setup-core.js";
|
||||
import { matrixSetupWizard } from "./setup-surface.js";
|
||||
import { runMatrixStartupMaintenance } from "./startup-maintenance.js";
|
||||
@@ -452,7 +458,16 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
||||
}),
|
||||
},
|
||||
actions: matrixMessageActions,
|
||||
setup: matrixSetupAdapter,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
setup: {
|
||||
...matrixSetupAdapter,
|
||||
singleAccountKeysToMove,
|
||||
namedAccountPromotionKeys,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
},
|
||||
bindings: {
|
||||
compileConfiguredBinding: ({ conversationId }) =>
|
||||
normalizeMatrixAcpConversationId(conversationId),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
|
||||
export const defaultMarkdownTableMode = "off";
|
||||
@@ -16,11 +16,11 @@ import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channe
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { mattermostApprovalAuth } from "./approval-auth.js";
|
||||
import {
|
||||
chunkTextForOutbound,
|
||||
@@ -52,6 +52,7 @@ import { collectMattermostSlashCallbackPaths } from "./mattermost/slash-commands
|
||||
import { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";
|
||||
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
|
||||
import { getMattermostRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { resolveMattermostOutboundSessionRoute } from "./session-route.js";
|
||||
import { mattermostSetupAdapter } from "./setup-core.js";
|
||||
import { mattermostSetupWizard } from "./setup-surface.js";
|
||||
@@ -336,6 +337,10 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
||||
resolveRequireMention: resolveMattermostGroupRequireMention,
|
||||
},
|
||||
actions: mattermostMessageActions,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
directory: createChannelDirectoryAdapter({
|
||||
listGroups: async (params) => listMattermostDirectoryGroups(params),
|
||||
listGroupsLive: async (params) => listMattermostDirectoryGroups(params),
|
||||
@@ -343,6 +348,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
||||
listPeersLive: async (params) => listMattermostDirectoryPeers(params),
|
||||
}),
|
||||
messaging: {
|
||||
defaultMarkdownTableMode: "off",
|
||||
normalizeTarget: normalizeMattermostMessagingTarget,
|
||||
resolveOutboundSessionRoute: (params) => resolveMattermostOutboundSessionRoute(params),
|
||||
targetResolver: {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
resolveMSTeamsUserAllowlist,
|
||||
} from "./resolve-allowlist.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { resolveMSTeamsOutboundSessionRoute } from "./session-route.js";
|
||||
import { msteamsSetupAdapter } from "./setup-core.js";
|
||||
import { msteamsSetupWizard } from "./setup-surface.js";
|
||||
@@ -396,6 +397,10 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
|
||||
collectMutableAllowlistWarnings: collectMSTeamsMutableAllowlistWarnings,
|
||||
},
|
||||
setup: msteamsSetupAdapter,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeMSTeamsMessagingTarget,
|
||||
resolveOutboundSessionRoute: (params) => resolveMSTeamsOutboundSessionRoute(params),
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "./normalize.js";
|
||||
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { sendMessageNextcloudTalk } from "./send.js";
|
||||
import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
|
||||
import { nextcloudTalkSetupAdapter } from "./setup-core.js";
|
||||
@@ -173,6 +174,10 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
hint: "<roomToken>",
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
setup: nextcloudTalkSetupAdapter,
|
||||
status: createComputedAccountStatusAdapter<ResolvedNextcloudTalkAccount>({
|
||||
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const defaultMarkdownTableMode = "bullets";
|
||||
@@ -71,8 +71,9 @@ export function createSignalPluginBase(params: {
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
> {
|
||||
return createChannelPluginBase({
|
||||
const base = createChannelPluginBase({
|
||||
id: SIGNAL_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(SIGNAL_CHANNEL),
|
||||
@@ -102,7 +103,13 @@ export function createSignalPluginBase(params: {
|
||||
},
|
||||
security: signalSecurityAdapter,
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
messaging: {
|
||||
defaultMarkdownTableMode: "bullets",
|
||||
},
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedSignalAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
@@ -114,5 +121,6 @@ export function createSignalPluginBase(params: {
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
| "messaging"
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
|
||||
(key) => typeof params.env?.[key] === "string" && params.env[key]?.trim().length > 0,
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { getChatChannelMeta, type ChannelPlugin, type OpenClawConfig } from "./c
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { slackDoctor } from "./doctor.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
|
||||
export const SLACK_CHANNEL = "slack" as const;
|
||||
|
||||
@@ -174,6 +175,7 @@ export function createSlackPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "secrets"
|
||||
> {
|
||||
return {
|
||||
id: SLACK_CHANNEL,
|
||||
@@ -225,6 +227,10 @@ export function createSlackPluginBase(params: {
|
||||
configSchema: SlackChannelConfigSchema,
|
||||
config: {
|
||||
...slackConfigAdapter,
|
||||
hasConfiguredState: ({ env }) =>
|
||||
["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
|
||||
(key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0,
|
||||
),
|
||||
isConfigured: (account) => isSlackPluginAccountConfigured(account),
|
||||
describeAccount: (account) =>
|
||||
describeAccountSnapshot({
|
||||
@@ -236,6 +242,10 @@ export function createSlackPluginBase(params: {
|
||||
},
|
||||
}),
|
||||
},
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
setup: params.setup,
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedSlackAccount>,
|
||||
@@ -251,5 +261,6 @@ export function createSlackPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "secrets"
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,19 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
normalizeTelegramCommandDescription,
|
||||
normalizeTelegramCommandName,
|
||||
resolveTelegramCustomCommands,
|
||||
} from "./src/command-config.js";
|
||||
export { singleAccountKeysToMove } from "./src/setup-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.TELEGRAM_BOT_TOKEN === "string" &&
|
||||
params.env.TELEGRAM_BOT_TOKEN.trim().length > 0
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
} from "./command-ui.js";
|
||||
import { TelegramChannelConfigSchema } from "./config-schema.js";
|
||||
import { telegramDoctor } from "./doctor.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { singleAccountKeysToMove } from "./setup-contract.js";
|
||||
|
||||
export const TELEGRAM_CHANNEL = "telegram" as const;
|
||||
|
||||
@@ -125,8 +127,9 @@ export function createTelegramPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "secrets"
|
||||
> {
|
||||
return createChannelPluginBase({
|
||||
const base = createChannelPluginBase({
|
||||
id: TELEGRAM_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(TELEGRAM_CHANNEL),
|
||||
@@ -155,6 +158,8 @@ export function createTelegramPluginBase(params: {
|
||||
configSchema: TelegramChannelConfigSchema,
|
||||
config: {
|
||||
...telegramConfigAdapter,
|
||||
hasConfiguredState: ({ env }) =>
|
||||
typeof env?.TELEGRAM_BOT_TOKEN === "string" && env.TELEGRAM_BOT_TOKEN.trim().length > 0,
|
||||
isConfigured: (account, cfg) => {
|
||||
// Use inspectTelegramAccount for a complete token resolution that includes
|
||||
// channel-level fallback paths not available in resolveTelegramAccount.
|
||||
@@ -218,8 +223,18 @@ export function createTelegramPluginBase(params: {
|
||||
};
|
||||
},
|
||||
},
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
setup: {
|
||||
...params.setup,
|
||||
singleAccountKeysToMove,
|
||||
},
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedTelegramAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
@@ -231,5 +246,6 @@ export function createTelegramPluginBase(params: {
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
| "secrets"
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
@@ -1,63 +0,0 @@
|
||||
type UnsupportedSecretRefConfigCandidate = {
|
||||
path: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export { normalizeCompatibilityConfig } from "./src/doctor-contract.js";
|
||||
import { hasAnyWhatsAppAuth } from "./src/accounts.js";
|
||||
export { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./src/session-contract.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export const unsupportedSecretRefSurfacePatterns = [
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
] as const;
|
||||
|
||||
export const defaultMarkdownTableMode = "bullets";
|
||||
|
||||
export { resolveLegacyGroupSessionKey } from "./src/group-session-contract.js";
|
||||
|
||||
export function hasPersistedAuthState(params: {
|
||||
cfg: import("openclaw/plugin-sdk/config-runtime").OpenClawConfig;
|
||||
}): boolean {
|
||||
return hasAnyWhatsAppAuth(params.cfg);
|
||||
}
|
||||
|
||||
export function collectUnsupportedSecretRefConfigCandidates(
|
||||
raw: unknown,
|
||||
): UnsupportedSecretRefConfigCandidate[] {
|
||||
if (!isRecord(raw)) {
|
||||
return [];
|
||||
}
|
||||
if (!isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: UnsupportedSecretRefConfigCandidate[] = [];
|
||||
const whatsapp = raw.channels.whatsapp;
|
||||
const creds = isRecord(whatsapp.creds) ? whatsapp.creds : null;
|
||||
if (creds) {
|
||||
candidates.push({
|
||||
path: "channels.whatsapp.creds.json",
|
||||
value: creds.json,
|
||||
});
|
||||
}
|
||||
|
||||
const accounts = isRecord(whatsapp.accounts) ? whatsapp.accounts : null;
|
||||
if (!accounts) {
|
||||
return candidates;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(accounts)) {
|
||||
if (!isRecord(account) || !isRecord(account.creds)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({
|
||||
path: `channels.whatsapp.accounts.${accountId}.creds.json`,
|
||||
value: account.creds.json,
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
@@ -27,9 +27,53 @@ import {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import { resolveLegacyGroupSessionKey } from "./group-session-contract.js";
|
||||
import { applyWhatsAppSecurityConfigFixes } from "./security-fix.js";
|
||||
import { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./session-contract.js";
|
||||
|
||||
export const WHATSAPP_CHANNEL = "whatsapp" as const;
|
||||
const WHATSAPP_UNSUPPORTED_SECRET_REF_SURFACE_PATTERNS = [
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function collectUnsupportedSecretRefConfigCandidates(raw: unknown): Array<{
|
||||
path: string;
|
||||
value: unknown;
|
||||
}> {
|
||||
if (!isRecord(raw) || !isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: Array<{ path: string; value: unknown }> = [];
|
||||
const whatsapp = raw.channels.whatsapp;
|
||||
const creds = isRecord(whatsapp.creds) ? whatsapp.creds : null;
|
||||
if (creds) {
|
||||
candidates.push({
|
||||
path: "channels.whatsapp.creds.json",
|
||||
value: creds.json,
|
||||
});
|
||||
}
|
||||
|
||||
const accounts = isRecord(whatsapp.accounts) ? whatsapp.accounts : null;
|
||||
if (!accounts) {
|
||||
return candidates;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(accounts)) {
|
||||
if (!isRecord(account) || !isRecord(account.creds)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({
|
||||
path: `channels.whatsapp.accounts.${accountId}.creds.json`,
|
||||
value: account.creds.json,
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export async function loadWhatsAppChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
@@ -172,6 +216,17 @@ export function createWhatsAppPluginBase(params: {
|
||||
gatewayMethods: base.gatewayMethods!,
|
||||
configSchema: base.configSchema!,
|
||||
config: base.config!,
|
||||
messaging: {
|
||||
defaultMarkdownTableMode: "bullets",
|
||||
resolveLegacyGroupSessionKey,
|
||||
isLegacyGroupSessionKey,
|
||||
canonicalizeLegacySessionKey: (params) =>
|
||||
canonicalizeLegacySessionKey({ key: params.key, agentId: params.agentId }),
|
||||
},
|
||||
secrets: {
|
||||
unsupportedSecretRefSurfacePatterns: WHATSAPP_UNSUPPORTED_SECRET_REF_SURFACE_PATTERNS,
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
},
|
||||
security: base.security!,
|
||||
groups: base.groups!,
|
||||
} satisfies Pick<
|
||||
@@ -184,6 +239,8 @@ export function createWhatsAppPluginBase(params: {
|
||||
| "gatewayMethods"
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "messaging"
|
||||
| "secrets"
|
||||
| "security"
|
||||
| "doctor"
|
||||
| "setup"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -43,6 +43,7 @@ import { zaloMessageActions } from "./actions.js";
|
||||
import { zaloApprovalAuth } from "./approval-auth.js";
|
||||
import { ZaloConfigSchema } from "./config-schema.js";
|
||||
import type { ZaloProbeResult } from "./probe.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { resolveZaloOutboundSessionRoute } from "./session-route.js";
|
||||
import { createZaloSetupWizardProxy, zaloSetupAdapter } from "./setup-core.js";
|
||||
import { collectZaloStatusIssues } from "./status-issues.js";
|
||||
@@ -198,6 +199,10 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount, ZaloProbeResult> =
|
||||
}),
|
||||
},
|
||||
auth: zaloApprovalAuth,
|
||||
secrets: {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { listBootstrapChannelPlugins } from "./plugins/bootstrap-registry.js";
|
||||
import { listBundledChannelPluginIds } from "./plugins/bundled-ids.js";
|
||||
import { listBundledChannelPlugins } from "./plugins/bundled.js";
|
||||
|
||||
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function listPotentialConfiguredChannelIds(
|
||||
}
|
||||
|
||||
if (options.includePersistedAuthState !== false && hasPersistedChannelState(env)) {
|
||||
for (const plugin of listBundledChannelPlugins()) {
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
if (plugin.config?.hasPersistedAuthState?.({ cfg, env })) {
|
||||
configuredChannelIds.add(plugin.id);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ function hasEnvConfiguredChannel(
|
||||
if (options.includePersistedAuthState === false || !hasPersistedChannelState(env)) {
|
||||
return false;
|
||||
}
|
||||
return listBundledChannelPlugins().some((plugin) =>
|
||||
return listBootstrapChannelPlugins().some((plugin) =>
|
||||
Boolean(plugin.config?.hasPersistedAuthState?.({ cfg, env })),
|
||||
);
|
||||
}
|
||||
|
||||
81
src/channels/plugins/bootstrap-registry.ts
Normal file
81
src/channels/plugins/bootstrap-registry.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { listBundledChannelPlugins, listBundledChannelSetupPlugins } from "./bundled.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
type CachedBootstrapPlugins = {
|
||||
sorted: ChannelPlugin[];
|
||||
byId: Map<string, ChannelPlugin>;
|
||||
};
|
||||
|
||||
let cachedBootstrapPlugins: CachedBootstrapPlugins | null = null;
|
||||
|
||||
function mergePluginSection<T>(
|
||||
runtimeValue: T | undefined,
|
||||
setupValue: T | undefined,
|
||||
): T | undefined {
|
||||
if (
|
||||
runtimeValue &&
|
||||
setupValue &&
|
||||
typeof runtimeValue === "object" &&
|
||||
typeof setupValue === "object"
|
||||
) {
|
||||
return {
|
||||
...(runtimeValue as Record<string, unknown>),
|
||||
...(setupValue as Record<string, unknown>),
|
||||
} as T;
|
||||
}
|
||||
return setupValue ?? runtimeValue;
|
||||
}
|
||||
|
||||
function mergeBootstrapPlugin(
|
||||
runtimePlugin: ChannelPlugin,
|
||||
setupPlugin: ChannelPlugin,
|
||||
): ChannelPlugin {
|
||||
return {
|
||||
...runtimePlugin,
|
||||
...setupPlugin,
|
||||
meta: mergePluginSection(runtimePlugin.meta, setupPlugin.meta),
|
||||
capabilities: mergePluginSection(runtimePlugin.capabilities, setupPlugin.capabilities),
|
||||
commands: mergePluginSection(runtimePlugin.commands, setupPlugin.commands),
|
||||
doctor: mergePluginSection(runtimePlugin.doctor, setupPlugin.doctor),
|
||||
reload: mergePluginSection(runtimePlugin.reload, setupPlugin.reload),
|
||||
config: mergePluginSection(runtimePlugin.config, setupPlugin.config),
|
||||
setup: mergePluginSection(runtimePlugin.setup, setupPlugin.setup),
|
||||
messaging: mergePluginSection(runtimePlugin.messaging, setupPlugin.messaging),
|
||||
actions: mergePluginSection(runtimePlugin.actions, setupPlugin.actions),
|
||||
secrets: mergePluginSection(runtimePlugin.secrets, setupPlugin.secrets),
|
||||
} as ChannelPlugin;
|
||||
}
|
||||
|
||||
function buildBootstrapPlugins(): CachedBootstrapPlugins {
|
||||
const byId = new Map<string, ChannelPlugin>();
|
||||
for (const plugin of listBundledChannelPlugins()) {
|
||||
byId.set(plugin.id, plugin);
|
||||
}
|
||||
for (const plugin of listBundledChannelSetupPlugins()) {
|
||||
const runtimePlugin = byId.get(plugin.id);
|
||||
byId.set(plugin.id, runtimePlugin ? mergeBootstrapPlugin(runtimePlugin, plugin) : plugin);
|
||||
}
|
||||
const sorted = [...byId.values()].toSorted((left, right) => left.id.localeCompare(right.id));
|
||||
return { sorted, byId };
|
||||
}
|
||||
|
||||
function getBootstrapPlugins(): CachedBootstrapPlugins {
|
||||
cachedBootstrapPlugins ??= buildBootstrapPlugins();
|
||||
return cachedBootstrapPlugins;
|
||||
}
|
||||
|
||||
export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
|
||||
return getBootstrapPlugins().sorted;
|
||||
}
|
||||
|
||||
export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
const resolvedId = String(id).trim();
|
||||
if (!resolvedId) {
|
||||
return undefined;
|
||||
}
|
||||
return getBootstrapPlugins().byId.get(resolvedId);
|
||||
}
|
||||
|
||||
export function clearBootstrapChannelPluginCache(): void {
|
||||
cachedBootstrapPlugins = null;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getBundledChannelContractSurfaceModule } from "./contract-surfaces.js";
|
||||
|
||||
describe("bundled channel contract surfaces", () => {
|
||||
it("resolves Telegram contract surfaces from a source checkout", () => {
|
||||
const surface = getBundledChannelContractSurfaceModule<{
|
||||
normalizeTelegramCommandName?: (value: string) => string;
|
||||
}>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
});
|
||||
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface?.normalizeTelegramCommandName?.("/Hello-World")).toBe("hello_world");
|
||||
});
|
||||
|
||||
it.each(["matrix", "mattermost", "bluebubbles", "nextcloud-talk", "tlon"])(
|
||||
"exposes legacy migration hooks for %s from a source checkout",
|
||||
(pluginId) => {
|
||||
const surface = getBundledChannelContractSurfaceModule<{
|
||||
normalizeCompatibilityConfig?: (params: { cfg: Record<string, unknown> }) => {
|
||||
config: Record<string, unknown>;
|
||||
changes: string[];
|
||||
};
|
||||
legacyConfigRules?: unknown[];
|
||||
}>({
|
||||
pluginId,
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
});
|
||||
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface?.normalizeCompatibilityConfig).toBeTypeOf("function");
|
||||
expect(Array.isArray(surface?.legacyConfigRules)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { discoverOpenClawPlugins } from "../../plugins/discovery.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
resolveLoaderPackageRoot,
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
const CONTRACT_SURFACE_BASENAMES = [
|
||||
"contract-surfaces.ts",
|
||||
"contract-surfaces.js",
|
||||
"contract-api.ts",
|
||||
"contract-api.js",
|
||||
] as const;
|
||||
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"] as const;
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveLoaderPackageRoot({
|
||||
modulePath: fileURLToPath(import.meta.url),
|
||||
moduleUrl: import.meta.url,
|
||||
}) ?? fileURLToPath(new URL("../../..", import.meta.url));
|
||||
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
|
||||
const RUNNING_FROM_BUILT_ARTIFACT =
|
||||
CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) ||
|
||||
CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`);
|
||||
|
||||
type ContractSurfaceBasename = (typeof CONTRACT_SURFACE_BASENAMES)[number];
|
||||
|
||||
let cachedSurfaces: unknown[] | null = null;
|
||||
let cachedSurfaceEntries: Array<{
|
||||
pluginId: string;
|
||||
surface: unknown;
|
||||
}> | null = null;
|
||||
const cachedPreferredSurfaceModules = new Map<string, unknown>();
|
||||
|
||||
function createModuleLoader() {
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
return (modulePath: string) => {
|
||||
const tryNative = shouldPreferNativeJiti(modulePath);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
aliasMap: Object.entries(aliasMap).toSorted(([a], [b]) => a.localeCompare(b)),
|
||||
});
|
||||
const cached = jitiLoaders.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const loader = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
jitiLoaders.set(cacheKey, loader);
|
||||
return loader;
|
||||
};
|
||||
}
|
||||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function getContractSurfaceDiscoveryEnv(): NodeJS.ProcessEnv {
|
||||
if (RUNNING_FROM_BUILT_ARTIFACT) {
|
||||
return process.env;
|
||||
}
|
||||
return {
|
||||
...process.env,
|
||||
VITEST: process.env.VITEST || "1",
|
||||
};
|
||||
}
|
||||
|
||||
function matchesPreferredBasename(
|
||||
basename: ContractSurfaceBasename,
|
||||
preferredBasename: ContractSurfaceBasename | undefined,
|
||||
): boolean {
|
||||
if (!preferredBasename) {
|
||||
return true;
|
||||
}
|
||||
return basename.replace(/\.[^.]+$/u, "") === preferredBasename.replace(/\.[^.]+$/u, "");
|
||||
}
|
||||
|
||||
function resolveDistPreferredModulePath(modulePath: string): string {
|
||||
const compiledDistModulePath = modulePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
return compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)
|
||||
? compiledDistModulePath
|
||||
: modulePath;
|
||||
}
|
||||
|
||||
function resolveContractSurfaceModulePaths(
|
||||
rootDir: string | undefined,
|
||||
preferredBasename?: ContractSurfaceBasename,
|
||||
): string[] {
|
||||
if (typeof rootDir !== "string" || rootDir.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const modulePaths: string[] = [];
|
||||
for (const basename of CONTRACT_SURFACE_BASENAMES) {
|
||||
if (!matchesPreferredBasename(basename, preferredBasename)) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = path.join(rootDir, basename);
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
continue;
|
||||
}
|
||||
modulePaths.push(resolveDistPreferredModulePath(modulePath));
|
||||
}
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
function resolveSourceFirstContractSurfaceModulePaths(params: {
|
||||
rootDir: string | undefined;
|
||||
preferredBasename?: ContractSurfaceBasename;
|
||||
}): string[] {
|
||||
if (typeof params.rootDir !== "string" || params.rootDir.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (RUNNING_FROM_BUILT_ARTIFACT) {
|
||||
return resolveContractSurfaceModulePaths(params.rootDir, params.preferredBasename);
|
||||
}
|
||||
|
||||
const dirName = path.basename(path.resolve(params.rootDir));
|
||||
const sourceRoot = path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", dirName);
|
||||
const modulePaths: string[] = [];
|
||||
|
||||
for (const basename of CONTRACT_SURFACE_BASENAMES) {
|
||||
if (!matchesPreferredBasename(basename, params.preferredBasename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceBaseName = basename.replace(/\.[^.]+$/u, "");
|
||||
let sourceCandidatePath: string | null = null;
|
||||
for (const ext of PUBLIC_SURFACE_SOURCE_EXTENSIONS) {
|
||||
const candidate = path.join(sourceRoot, `${sourceBaseName}${ext}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
sourceCandidatePath = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sourceCandidatePath) {
|
||||
modulePaths.push(sourceCandidatePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const builtCandidates = resolveContractSurfaceModulePaths(params.rootDir, basename);
|
||||
if (builtCandidates[0]) {
|
||||
modulePaths.push(builtCandidates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
function loadBundledChannelContractSurfaces(): unknown[] {
|
||||
return loadBundledChannelContractSurfaceEntries().map((entry) => entry.surface);
|
||||
}
|
||||
|
||||
function loadBundledChannelContractSurfaceEntries(): Array<{
|
||||
pluginId: string;
|
||||
surface: unknown;
|
||||
}> {
|
||||
const env = getContractSurfaceDiscoveryEnv();
|
||||
const discovery = discoverOpenClawPlugins({ cache: false, env });
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
cache: false,
|
||||
env,
|
||||
config: {},
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
const surfaces: Array<{ pluginId: string; surface: unknown }> = [];
|
||||
for (const manifest of manifestRegistry.plugins) {
|
||||
if (manifest.origin !== "bundled" || manifest.channels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = resolveSourceFirstContractSurfaceModulePaths({
|
||||
rootDir: manifest.rootDir,
|
||||
})[0];
|
||||
if (!modulePath) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
surfaces.push({
|
||||
pluginId: manifest.id,
|
||||
surface: loadModule(modulePath)(modulePath),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return surfaces;
|
||||
}
|
||||
|
||||
export function getBundledChannelContractSurfaces(): unknown[] {
|
||||
cachedSurfaces ??= loadBundledChannelContractSurfaces();
|
||||
return cachedSurfaces;
|
||||
}
|
||||
|
||||
export function getBundledChannelContractSurfaceEntries(): Array<{
|
||||
pluginId: string;
|
||||
surface: unknown;
|
||||
}> {
|
||||
cachedSurfaceEntries ??= loadBundledChannelContractSurfaceEntries();
|
||||
return cachedSurfaceEntries;
|
||||
}
|
||||
|
||||
export function getBundledChannelContractSurfaceModule<T = unknown>(params: {
|
||||
pluginId: string;
|
||||
preferredBasename?: ContractSurfaceBasename;
|
||||
}): T | null {
|
||||
const cacheKey = `${params.pluginId}:${params.preferredBasename ?? "*"}`;
|
||||
if (cachedPreferredSurfaceModules.has(cacheKey)) {
|
||||
return (cachedPreferredSurfaceModules.get(cacheKey) ?? null) as T | null;
|
||||
}
|
||||
const env = getContractSurfaceDiscoveryEnv();
|
||||
const discovery = discoverOpenClawPlugins({ cache: false, env });
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
cache: false,
|
||||
env,
|
||||
config: {},
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
const manifest = manifestRegistry.plugins.find(
|
||||
(entry) =>
|
||||
entry.origin === "bundled" && entry.channels.length > 0 && entry.id === params.pluginId,
|
||||
);
|
||||
if (!manifest) {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
const modulePath = resolveSourceFirstContractSurfaceModulePaths({
|
||||
rootDir: manifest.rootDir,
|
||||
preferredBasename: params.preferredBasename,
|
||||
})[0];
|
||||
if (!modulePath) {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const module = loadModule(modulePath)(modulePath) as T;
|
||||
cachedPreferredSurfaceModules.set(cacheKey, module);
|
||||
return module;
|
||||
} catch {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { LegacyConfigRule } from "../../config/legacy.shared.js";
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
import { getBundledChannelContractSurfaces } from "./contract-surfaces.js";
|
||||
|
||||
type ChannelLegacyConfigSurface = {
|
||||
legacyConfigRules?: LegacyConfigRule[];
|
||||
normalizeCompatibilityConfig?: (params: { cfg: OpenClawConfig }) => {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
warnings?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
function getChannelLegacyConfigSurfaces(): ChannelLegacyConfigSurface[] {
|
||||
return getBundledChannelContractSurfaces() as ChannelLegacyConfigSurface[];
|
||||
}
|
||||
import { listBootstrapChannelPlugins } from "./bootstrap-registry.js";
|
||||
|
||||
export function collectChannelLegacyConfigRules(): LegacyConfigRule[] {
|
||||
return getChannelLegacyConfigSurfaces().flatMap((surface) => surface.legacyConfigRules ?? []);
|
||||
return listBootstrapChannelPlugins().flatMap((plugin) => plugin.doctor?.legacyConfigRules ?? []);
|
||||
}
|
||||
|
||||
export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, unknown>): {
|
||||
@@ -25,8 +12,8 @@ export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, un
|
||||
} {
|
||||
let nextCfg = cfg as OpenClawConfig & Record<string, unknown>;
|
||||
const changes: string[] = [];
|
||||
for (const surface of getChannelLegacyConfigSurfaces()) {
|
||||
const mutation = surface.normalizeCompatibilityConfig?.({ cfg: nextCfg });
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
const mutation = plugin.doctor?.normalizeCompatibilityConfig?.({ cfg: nextCfg });
|
||||
if (!mutation || mutation.changes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,53 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { z, type ZodType } from "zod";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { getBootstrapChannelPlugin } from "./bootstrap-registry.js";
|
||||
import type { ChannelSetupAdapter } from "./types.adapters.js";
|
||||
import type { ChannelSetupInput } from "./types.core.js";
|
||||
|
||||
const SETUP_PROMOTION_SURFACE_MODULE_BASENAMES = [
|
||||
"contract-surfaces.ts",
|
||||
"contract-surfaces.js",
|
||||
] as const;
|
||||
|
||||
type SetupPromotionRuntimeModule = Pick<
|
||||
typeof import("./contract-surfaces.js"),
|
||||
"getBundledChannelContractSurfaceEntries"
|
||||
>;
|
||||
|
||||
let cachedSetupPromotionRuntimeModule: SetupPromotionRuntimeModule | null = null;
|
||||
|
||||
export function clearSetupPromotionRuntimeModuleCache(): void {
|
||||
cachedSetupPromotionRuntimeModule = null;
|
||||
}
|
||||
|
||||
function resolveSetupPromotionRuntimeModulePath(): string {
|
||||
for (const basename of SETUP_PROMOTION_SURFACE_MODULE_BASENAMES) {
|
||||
const candidatePath = fileURLToPath(new URL(basename, import.meta.url));
|
||||
const resolvedPath = candidatePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
throw new Error("missing setup promotion runtime module");
|
||||
}
|
||||
|
||||
function loadSetupPromotionRuntimeModule(): SetupPromotionRuntimeModule {
|
||||
cachedSetupPromotionRuntimeModule ??= createJiti(import.meta.url)(
|
||||
resolveSetupPromotionRuntimeModulePath(),
|
||||
) as SetupPromotionRuntimeModule;
|
||||
return cachedSetupPromotionRuntimeModule;
|
||||
}
|
||||
|
||||
type ChannelSectionBase = {
|
||||
name?: string;
|
||||
defaultAccount?: string;
|
||||
@@ -193,6 +150,8 @@ export function prepareScopedSetupConfig(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function clearSetupPromotionRuntimeModuleCache(): void {}
|
||||
|
||||
export function applySetupAccountConfigPatch(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelKey: string;
|
||||
@@ -458,13 +417,11 @@ type ChannelSetupPromotionSurface = {
|
||||
};
|
||||
|
||||
function getChannelSetupPromotionSurface(channelKey: string): ChannelSetupPromotionSurface | null {
|
||||
const entry = loadSetupPromotionRuntimeModule()
|
||||
.getBundledChannelContractSurfaceEntries()
|
||||
.find((candidate) => candidate.pluginId === channelKey);
|
||||
if (!entry || !entry.surface || typeof entry.surface !== "object") {
|
||||
const setup = getBootstrapChannelPlugin(channelKey)?.setup;
|
||||
if (!setup || typeof setup !== "object") {
|
||||
return null;
|
||||
}
|
||||
return entry.surface as ChannelSetupPromotionSurface;
|
||||
return setup as ChannelSetupPromotionSurface;
|
||||
}
|
||||
|
||||
export function shouldMoveSingleAccountChannelKey(params: {
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { ResolverContext, SecretDefaults } from "../../secrets/runtime-shared.js";
|
||||
import type { SecretTargetRegistryEntry } from "../../secrets/target-registry-types.js";
|
||||
import type { ConfigWriteTarget } from "./config-writes.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
@@ -95,6 +97,11 @@ export type ChannelSetupAdapter = {
|
||||
accountId: string;
|
||||
input: ChannelSetupInput;
|
||||
}) => string | null;
|
||||
singleAccountKeysToMove?: readonly string[];
|
||||
namedAccountPromotionKeys?: readonly string[];
|
||||
resolveSingleAccountPromotionTarget?: (params: {
|
||||
channel: Record<string, unknown>;
|
||||
}) => string | undefined;
|
||||
};
|
||||
|
||||
export type ChannelConfigAdapter<ResolvedAccount> = {
|
||||
@@ -122,6 +129,7 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
|
||||
accountId?: string | null;
|
||||
allowFrom: Array<string | number>;
|
||||
}) => string[];
|
||||
hasConfiguredState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
hasPersistedAuthState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
resolveDefaultTo?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -129,6 +137,20 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
|
||||
}) => string | undefined;
|
||||
};
|
||||
|
||||
export type ChannelSecretsAdapter = {
|
||||
secretTargetRegistryEntries?: readonly SecretTargetRegistryEntry[];
|
||||
unsupportedSecretRefSurfacePatterns?: readonly string[];
|
||||
collectUnsupportedSecretRefConfigCandidates?: (raw: unknown) => Array<{
|
||||
path: string;
|
||||
value: unknown;
|
||||
}>;
|
||||
collectRuntimeConfigAssignments?: (params: {
|
||||
config: OpenClawConfig;
|
||||
defaults: SecretDefaults | undefined;
|
||||
context: ResolverContext;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type ChannelGroupAdapter = {
|
||||
resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
|
||||
resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TSchema } from "@sinclair/typebox";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { MarkdownTableMode } from "../../config/types.base.js";
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import type { PollInput } from "../../polls.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
@@ -404,10 +405,31 @@ export type ChannelThreadingToolContext = {
|
||||
/** Channel-owned messaging helpers for target parsing, routing, and payload shaping. */
|
||||
export type ChannelMessagingAdapter = {
|
||||
normalizeTarget?: (raw: string) => string | undefined;
|
||||
defaultMarkdownTableMode?: MarkdownTableMode;
|
||||
normalizeExplicitSessionKey?: (params: {
|
||||
sessionKey: string;
|
||||
ctx: MsgContext;
|
||||
}) => string | undefined;
|
||||
deriveLegacySessionChatType?: (sessionKey: string) => "direct" | "group" | "channel" | undefined;
|
||||
isLegacyGroupSessionKey?: (key: string) => boolean;
|
||||
canonicalizeLegacySessionKey?: (params: {
|
||||
key: string;
|
||||
agentId: string;
|
||||
}) => string | null | undefined;
|
||||
resolveLegacyGroupSessionKey?: (ctx: MsgContext) => {
|
||||
key: string;
|
||||
channel: string;
|
||||
id: string;
|
||||
chatType: "group" | "channel";
|
||||
} | null;
|
||||
resolveInboundAttachmentRoots?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => string[];
|
||||
resolveRemoteInboundAttachmentRoots?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => string[];
|
||||
resolveInboundConversation?: (params: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
@@ -606,6 +628,14 @@ export type ChannelMessageActionAdapter = {
|
||||
action: ChannelMessageActionName;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
messageActionTargetAliases?: Partial<
|
||||
Record<
|
||||
ChannelMessageActionName,
|
||||
{
|
||||
aliases: string[];
|
||||
}
|
||||
>
|
||||
>;
|
||||
requiresTrustedRequesterSender?: (params: {
|
||||
action: ChannelMessageActionName;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ChannelLifecycleAdapter,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPairingAdapter,
|
||||
ChannelSecretsAdapter,
|
||||
ChannelSecurityAdapter,
|
||||
ChannelSetupAdapter,
|
||||
ChannelStatusAdapter,
|
||||
@@ -106,6 +107,7 @@ export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknow
|
||||
elevated?: ChannelElevatedAdapter;
|
||||
commands?: ChannelCommandAdapter;
|
||||
lifecycle?: ChannelLifecycleAdapter;
|
||||
secrets?: ChannelSecretsAdapter;
|
||||
approvals?: ChannelApprovalAdapter;
|
||||
allowlist?: ChannelAllowlistAdapter;
|
||||
doctor?: ChannelDoctorAdapter;
|
||||
|
||||
@@ -37,6 +37,7 @@ export type {
|
||||
ChannelOutboundPayloadHint,
|
||||
ChannelOutboundTargetRef,
|
||||
ChannelAllowlistAdapter,
|
||||
ChannelSecretsAdapter,
|
||||
ChannelCommandConversationContext,
|
||||
ChannelConfiguredBindingConversationRef,
|
||||
ChannelConfiguredBindingMatch,
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||
@@ -463,7 +463,7 @@ function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boo
|
||||
if (!isRecord(channels)) {
|
||||
return false;
|
||||
}
|
||||
for (const plugin of listBundledChannelPlugins()) {
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
if (plugin.config.hasPersistedAuthState?.({ cfg, env })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
|
||||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
type ChannelConfiguredSurface = {
|
||||
hasConfiguredState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
hasPersistedAuthState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
};
|
||||
|
||||
function resolveChannelConfig(
|
||||
cfg: OpenClawConfig,
|
||||
channelId: string,
|
||||
@@ -22,24 +17,17 @@ function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boo
|
||||
return hasMeaningfulChannelConfig(entry);
|
||||
}
|
||||
|
||||
function getChannelConfiguredSurface(channelId: string): ChannelConfiguredSurface | null {
|
||||
return getBundledChannelContractSurfaceModule<ChannelConfiguredSurface>({
|
||||
pluginId: channelId,
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
});
|
||||
}
|
||||
|
||||
export function isChannelConfigured(
|
||||
cfg: OpenClawConfig,
|
||||
channelId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const surface = getChannelConfiguredSurface(channelId);
|
||||
const pluginConfigured = surface?.hasConfiguredState?.({ cfg, env });
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
const pluginConfigured = plugin?.config?.hasConfiguredState?.({ cfg, env });
|
||||
if (pluginConfigured) {
|
||||
return true;
|
||||
}
|
||||
const pluginPersistedAuthState = surface?.hasPersistedAuthState?.({ cfg, env });
|
||||
const pluginPersistedAuthState = plugin?.config?.hasPersistedAuthState?.({ cfg, env });
|
||||
if (pluginPersistedAuthState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBundledChannelContractSurfaceEntries } from "../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { resolveAccountEntry } from "../routing/account-lookup.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
@@ -15,17 +15,12 @@ type MarkdownConfigSection = MarkdownConfigEntry & {
|
||||
accounts?: Record<string, MarkdownConfigEntry>;
|
||||
};
|
||||
|
||||
type ChannelMarkdownTableSurface = {
|
||||
defaultMarkdownTableMode?: MarkdownTableMode;
|
||||
};
|
||||
|
||||
function buildDefaultTableModes(): Map<string, MarkdownTableMode> {
|
||||
return new Map(
|
||||
getBundledChannelContractSurfaceEntries()
|
||||
.flatMap(({ pluginId, surface }) => {
|
||||
const defaultMarkdownTableMode = (surface as ChannelMarkdownTableSurface)
|
||||
.defaultMarkdownTableMode;
|
||||
return defaultMarkdownTableMode ? [[pluginId, defaultMarkdownTableMode] as const] : [];
|
||||
listBootstrapChannelPlugins()
|
||||
.flatMap((plugin) => {
|
||||
const defaultMarkdownTableMode = plugin.messaging?.defaultMarkdownTableMode;
|
||||
return defaultMarkdownTableMode ? [[plugin.id, defaultMarkdownTableMode] as const] : [];
|
||||
})
|
||||
.toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { getBundledChannelContractSurfaces } from "../../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../../channels/plugins/bootstrap-registry.js";
|
||||
import { normalizeHyphenSlug } from "../../shared/string-normalization.js";
|
||||
import { listDeliverableMessageChannels } from "../../utils/message-channel.js";
|
||||
import type { GroupKeyResolution } from "./types.js";
|
||||
@@ -11,8 +11,10 @@ type LegacyGroupSessionSurface = {
|
||||
};
|
||||
|
||||
function resolveLegacyGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||
for (const surface of getBundledChannelContractSurfaces() as LegacyGroupSessionSurface[]) {
|
||||
const resolved = surface.resolveLegacyGroupSessionKey?.(ctx);
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
const resolved = (
|
||||
plugin.messaging as LegacyGroupSessionSurface | undefined
|
||||
)?.resolveLegacyGroupSessionKey?.(ctx);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBundledChannelContractSurfaceEntries } from "../../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../../channels/plugins/bootstrap-registry.js";
|
||||
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
|
||||
|
||||
export type MessageActionTargetMode = "to" | "channelId" | "none";
|
||||
@@ -78,20 +78,6 @@ const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, ActionTarg
|
||||
leaveGroup: { aliases: ["chatGuid", "chatIdentifier", "chatId"] },
|
||||
};
|
||||
|
||||
type ChannelMessageActionAliasSurface = {
|
||||
messageActionTargetAliases?: Partial<Record<ChannelMessageActionName, ActionTargetAliasSpec>>;
|
||||
};
|
||||
|
||||
function listChannelMessageActionAliasSurfaces(): Array<{
|
||||
pluginId: string;
|
||||
surface: ChannelMessageActionAliasSurface;
|
||||
}> {
|
||||
return getBundledChannelContractSurfaceEntries() as Array<{
|
||||
pluginId: string;
|
||||
surface: ChannelMessageActionAliasSurface;
|
||||
}>;
|
||||
}
|
||||
|
||||
function listActionTargetAliasSpecs(
|
||||
action: ChannelMessageActionName,
|
||||
channel?: string,
|
||||
@@ -105,11 +91,11 @@ function listActionTargetAliasSpecs(
|
||||
if (!normalizedChannel) {
|
||||
return specs;
|
||||
}
|
||||
for (const entry of listChannelMessageActionAliasSurfaces()) {
|
||||
if (entry.pluginId !== normalizedChannel) {
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
if (plugin.id !== normalizedChannel) {
|
||||
continue;
|
||||
}
|
||||
const channelSpec = entry.surface.messageActionTargetAliases?.[action];
|
||||
const channelSpec = plugin.actions?.messageActionTargetAliases?.[action];
|
||||
if (channelSpec) {
|
||||
specs.push(channelSpec);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
|
||||
import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js";
|
||||
import type { ChannelLegacyStateMigrationPlan } from "../channels/plugins/types.core.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -79,9 +79,11 @@ type LegacySessionSurface = {
|
||||
};
|
||||
|
||||
function getLegacySessionSurfaces(): LegacySessionSurface[] {
|
||||
return getBundledChannelContractSurfaces().filter(
|
||||
(surface): surface is LegacySessionSurface => Boolean(surface) && typeof surface === "object",
|
||||
);
|
||||
return listBootstrapChannelPlugins()
|
||||
.map((plugin) => plugin.messaging)
|
||||
.filter(
|
||||
(surface): surface is LegacySessionSurface => Boolean(surface) && typeof surface === "object",
|
||||
);
|
||||
}
|
||||
|
||||
function isSurfaceGroupKey(key: string): boolean {
|
||||
|
||||
@@ -1,41 +1,26 @@
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import { getBundledChannelContractSurfaceEntries } from "../channels/plugins/contract-surfaces.js";
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
type ChannelInboundMediaRootsSurface = {
|
||||
resolveInboundAttachmentRoots?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => string[];
|
||||
resolveRemoteInboundAttachmentRoots?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => string[];
|
||||
};
|
||||
|
||||
function normalizeChannelId(value?: string | null): string | undefined {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function findChannelMediaSurface(
|
||||
channelId?: string | null,
|
||||
): ChannelInboundMediaRootsSurface | undefined {
|
||||
function findChannelMessagingAdapter(channelId?: string | null) {
|
||||
const normalized = normalizeChannelId(channelId);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return getBundledChannelContractSurfaceEntries().find(
|
||||
(entry) => normalizeChannelId(entry.pluginId) === normalized,
|
||||
)?.surface as ChannelInboundMediaRootsSurface | undefined;
|
||||
return getBootstrapChannelPlugin(normalized)?.messaging;
|
||||
}
|
||||
|
||||
export function resolveChannelInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctx: MsgContext;
|
||||
}): readonly string[] | undefined {
|
||||
const surface = findChannelMediaSurface(params.ctx.Surface ?? params.ctx.Provider);
|
||||
return surface?.resolveInboundAttachmentRoots?.({
|
||||
const messaging = findChannelMessagingAdapter(params.ctx.Surface ?? params.ctx.Provider);
|
||||
return messaging?.resolveInboundAttachmentRoots?.({
|
||||
cfg: params.cfg,
|
||||
accountId: params.ctx.AccountId,
|
||||
});
|
||||
@@ -45,8 +30,8 @@ export function resolveChannelRemoteInboundAttachmentRoots(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctx: MsgContext;
|
||||
}): readonly string[] | undefined {
|
||||
const surface = findChannelMediaSurface(params.ctx.Surface ?? params.ctx.Provider);
|
||||
return surface?.resolveRemoteInboundAttachmentRoots?.({
|
||||
const messaging = findChannelMessagingAdapter(params.ctx.Surface ?? params.ctx.Provider);
|
||||
return messaging?.resolveRemoteInboundAttachmentRoots?.({
|
||||
cfg: params.cfg,
|
||||
accountId: params.ctx.AccountId,
|
||||
});
|
||||
|
||||
@@ -1,86 +1,22 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TELEGRAM_COMMAND_NAME_PATTERN as bundledTelegramCommandNamePattern } from "../../extensions/telegram/channel-config-api.ts";
|
||||
import * as telegramCommandConfig from "./telegram-command-config.js";
|
||||
|
||||
type BundledChannelContractSurfaceParams = Parameters<
|
||||
(typeof import("../channels/plugins/contract-surfaces.js"))["getBundledChannelContractSurfaceModule"]
|
||||
>[0];
|
||||
|
||||
type TelegramCommandConfigContract = Pick<
|
||||
typeof import("./telegram-command-config.js"),
|
||||
| "TELEGRAM_COMMAND_NAME_PATTERN"
|
||||
| "normalizeTelegramCommandName"
|
||||
| "normalizeTelegramCommandDescription"
|
||||
| "resolveTelegramCustomCommands"
|
||||
>;
|
||||
|
||||
const getBundledChannelContractSurfaceModule = vi.fn<
|
||||
(params: BundledChannelContractSurfaceParams) => TelegramCommandConfigContract | null
|
||||
>(() => null);
|
||||
|
||||
vi.mock("../channels/plugins/contract-surfaces.js", () => ({
|
||||
getBundledChannelContractSurfaceModule,
|
||||
}));
|
||||
|
||||
async function loadTelegramCommandConfig() {
|
||||
vi.resetModules();
|
||||
getBundledChannelContractSurfaceModule.mockClear();
|
||||
return import("./telegram-command-config.js");
|
||||
}
|
||||
|
||||
describe("telegram command config fallback", () => {
|
||||
it("keeps the fallback regex in parity with the bundled telegram contract", async () => {
|
||||
const telegramCommandConfig = await loadTelegramCommandConfig();
|
||||
|
||||
describe("telegram command config", () => {
|
||||
it("keeps the regex in parity with the bundled telegram contract", () => {
|
||||
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.toString()).toBe(
|
||||
bundledTelegramCommandNamePattern.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps import-time regex access side-effect free", async () => {
|
||||
const telegramCommandConfig = await loadTelegramCommandConfig();
|
||||
|
||||
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
|
||||
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
|
||||
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lazy-loads the contract pattern only when callers opt in", async () => {
|
||||
const contractPattern = /^[a-z]+$/;
|
||||
getBundledChannelContractSurfaceModule.mockReturnValueOnce({
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: contractPattern,
|
||||
normalizeTelegramCommandName: (value: string) => `contract:${value.trim().toLowerCase()}`,
|
||||
normalizeTelegramCommandDescription: (value: string) => `desc:${value.trim()}`,
|
||||
resolveTelegramCustomCommands: () => ({
|
||||
commands: [{ command: "from_contract", description: "from contract" }],
|
||||
issues: [],
|
||||
}),
|
||||
});
|
||||
const telegramCommandConfig = await loadTelegramCommandConfig();
|
||||
|
||||
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
|
||||
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
|
||||
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
|
||||
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(contractPattern);
|
||||
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
|
||||
it("exposes the same regex via the helper", () => {
|
||||
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(
|
||||
telegramCommandConfig.getTelegramCommandNamePattern(),
|
||||
telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
);
|
||||
expect(telegramCommandConfig.normalizeTelegramCommandName(" Hello ")).toBe("contract:hello");
|
||||
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("desc:hi");
|
||||
expect(
|
||||
telegramCommandConfig.resolveTelegramCustomCommands({
|
||||
commands: [{ command: "/ignored", description: "ignored" }],
|
||||
}),
|
||||
).toEqual({
|
||||
commands: [{ command: "from_contract", description: "from contract" }],
|
||||
issues: [],
|
||||
});
|
||||
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
|
||||
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps command validation available when the bundled contract surface is unavailable", async () => {
|
||||
const telegramCommandConfig = await loadTelegramCommandConfig();
|
||||
|
||||
it("validates and normalizes commands", () => {
|
||||
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
|
||||
expect(telegramCommandConfig.normalizeTelegramCommandName("/Hello-World")).toBe("hello_world");
|
||||
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("hi");
|
||||
@@ -114,6 +50,5 @@ describe("telegram command config fallback", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
|
||||
export type TelegramCustomCommandInput = {
|
||||
command?: string | null;
|
||||
description?: string | null;
|
||||
@@ -10,26 +8,9 @@ export type TelegramCustomCommandIssue = {
|
||||
field: "command" | "description";
|
||||
message: string;
|
||||
};
|
||||
const TELEGRAM_COMMAND_NAME_PATTERN_VALUE = /^[a-z0-9_]{1,32}$/;
|
||||
|
||||
type TelegramCommandConfigContract = {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: RegExp;
|
||||
normalizeTelegramCommandName: (value: string) => string;
|
||||
normalizeTelegramCommandDescription: (value: string) => string;
|
||||
resolveTelegramCustomCommands: (params: {
|
||||
commands?: TelegramCustomCommandInput[] | null;
|
||||
reservedCommands?: Set<string>;
|
||||
checkReserved?: boolean;
|
||||
checkDuplicates?: boolean;
|
||||
}) => {
|
||||
commands: Array<{ command: string; description: string }>;
|
||||
issues: TelegramCustomCommandIssue[];
|
||||
};
|
||||
};
|
||||
|
||||
const FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
||||
let cachedTelegramCommandConfigContract: TelegramCommandConfigContract | null = null;
|
||||
|
||||
function fallbackNormalizeTelegramCommandName(value: string): string {
|
||||
function normalizeTelegramCommandNameImpl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
@@ -38,11 +19,11 @@ function fallbackNormalizeTelegramCommandName(value: string): string {
|
||||
return withoutSlash.trim().toLowerCase().replace(/-/g, "_");
|
||||
}
|
||||
|
||||
function fallbackNormalizeTelegramCommandDescription(value: string): string {
|
||||
function normalizeTelegramCommandDescriptionImpl(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function fallbackResolveTelegramCustomCommands(params: {
|
||||
function resolveTelegramCustomCommandsImpl(params: {
|
||||
commands?: TelegramCustomCommandInput[] | null;
|
||||
reservedCommands?: Set<string>;
|
||||
checkReserved?: boolean;
|
||||
@@ -61,7 +42,7 @@ function fallbackResolveTelegramCustomCommands(params: {
|
||||
|
||||
for (let index = 0; index < entries.length; index += 1) {
|
||||
const entry = entries[index];
|
||||
const normalized = fallbackNormalizeTelegramCommandName(String(entry?.command ?? ""));
|
||||
const normalized = normalizeTelegramCommandNameImpl(String(entry?.command ?? ""));
|
||||
if (!normalized) {
|
||||
issues.push({
|
||||
index,
|
||||
@@ -70,7 +51,7 @@ function fallbackResolveTelegramCustomCommands(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
|
||||
if (!TELEGRAM_COMMAND_NAME_PATTERN_VALUE.test(normalized)) {
|
||||
issues.push({
|
||||
index,
|
||||
field: "command",
|
||||
@@ -94,9 +75,7 @@ function fallbackResolveTelegramCustomCommands(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const description = fallbackNormalizeTelegramCommandDescription(
|
||||
String(entry?.description ?? ""),
|
||||
);
|
||||
const description = normalizeTelegramCommandDescriptionImpl(String(entry?.description ?? ""));
|
||||
if (!description) {
|
||||
issues.push({
|
||||
index,
|
||||
@@ -114,38 +93,18 @@ function fallbackResolveTelegramCustomCommands(params: {
|
||||
return { commands: resolved, issues };
|
||||
}
|
||||
|
||||
const FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT: TelegramCommandConfigContract = {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
normalizeTelegramCommandName: fallbackNormalizeTelegramCommandName,
|
||||
normalizeTelegramCommandDescription: fallbackNormalizeTelegramCommandDescription,
|
||||
resolveTelegramCustomCommands: fallbackResolveTelegramCustomCommands,
|
||||
};
|
||||
|
||||
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
|
||||
cachedTelegramCommandConfigContract ??=
|
||||
getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
}) ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
|
||||
return cachedTelegramCommandConfigContract;
|
||||
}
|
||||
|
||||
export function getTelegramCommandNamePattern(): RegExp {
|
||||
return loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
|
||||
return TELEGRAM_COMMAND_NAME_PATTERN_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getTelegramCommandNamePattern()` when you need the live
|
||||
* bundled contract value. This export remains an import-time-safe fallback.
|
||||
*/
|
||||
export const TELEGRAM_COMMAND_NAME_PATTERN = FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN;
|
||||
export const TELEGRAM_COMMAND_NAME_PATTERN = TELEGRAM_COMMAND_NAME_PATTERN_VALUE;
|
||||
|
||||
export function normalizeTelegramCommandName(value: string): string {
|
||||
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);
|
||||
return normalizeTelegramCommandNameImpl(value);
|
||||
}
|
||||
|
||||
export function normalizeTelegramCommandDescription(value: string): string {
|
||||
return loadTelegramCommandConfigContract().normalizeTelegramCommandDescription(value);
|
||||
return normalizeTelegramCommandDescriptionImpl(value);
|
||||
}
|
||||
|
||||
export function resolveTelegramCustomCommands(params: {
|
||||
@@ -157,5 +116,5 @@ export function resolveTelegramCustomCommands(params: {
|
||||
commands: Array<{ command: string; description: string }>;
|
||||
issues: TelegramCustomCommandIssue[];
|
||||
} {
|
||||
return loadTelegramCommandConfigContract().resolveTelegramCustomCommands(params);
|
||||
return resolveTelegramCustomCommandsImpl(params);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
|
||||
type TelegramCommandUiContract = {
|
||||
buildCommandsPaginationKeyboard: (
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
agentId?: string,
|
||||
) => Array<Array<{ text: string; callback_data: string }>>;
|
||||
};
|
||||
|
||||
function loadTelegramCommandUiContract(): TelegramCommandUiContract {
|
||||
const contract = getBundledChannelContractSurfaceModule<TelegramCommandUiContract>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-api.ts",
|
||||
});
|
||||
if (!contract) {
|
||||
throw new Error("telegram command ui contract surface is unavailable");
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
export function buildCommandsPaginationKeyboard(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
agentId?: string,
|
||||
): Array<Array<{ text: string; callback_data: string }>> {
|
||||
return loadTelegramCommandUiContract().buildCommandsPaginationKeyboard(
|
||||
currentPage,
|
||||
totalPages,
|
||||
agentId,
|
||||
);
|
||||
const buttons: Array<{ text: string; callback_data: string }> = [];
|
||||
const suffix = agentId ? `:${agentId}` : "";
|
||||
|
||||
if (currentPage > 1) {
|
||||
buttons.push({
|
||||
text: "◀ Prev",
|
||||
callback_data: `commands_page_${currentPage - 1}${suffix}`,
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: `${currentPage}/${totalPages}`,
|
||||
callback_data: `commands_page_noop${suffix}`,
|
||||
});
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
buttons.push({
|
||||
text: "Next ▶",
|
||||
callback_data: `commands_page_${currentPage + 1}${suffix}`,
|
||||
});
|
||||
}
|
||||
|
||||
return [buttons];
|
||||
}
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js";
|
||||
|
||||
type ChannelRuntimeConfigCollectorSurface = {
|
||||
collectRuntimeConfigAssignments?: (params: {
|
||||
config: OpenClawConfig;
|
||||
defaults: SecretDefaults | undefined;
|
||||
context: ResolverContext;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
function listChannelRuntimeConfigCollectorSurfaces(): ChannelRuntimeConfigCollectorSurface[] {
|
||||
return getBundledChannelContractSurfaces() as ChannelRuntimeConfigCollectorSurface[];
|
||||
}
|
||||
|
||||
export function collectChannelConfigAssignments(params: {
|
||||
config: OpenClawConfig;
|
||||
defaults: SecretDefaults | undefined;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
for (const surface of listChannelRuntimeConfigCollectorSurfaces()) {
|
||||
surface.collectRuntimeConfigAssignments?.(params);
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
plugin.secrets?.collectRuntimeConfigAssignments?.(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { SecretTargetRegistryEntry } from "./target-registry-types.js";
|
||||
|
||||
type ChannelSecretTargetRegistrySurface = {
|
||||
secretTargetRegistryEntries?: readonly SecretTargetRegistryEntry[];
|
||||
};
|
||||
|
||||
const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret
|
||||
const SIBLING_REF_SHAPE = "sibling_ref"; // pragma: allowlist secret
|
||||
|
||||
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
||||
return (getBundledChannelContractSurfaces() as ChannelSecretTargetRegistrySurface[]).flatMap(
|
||||
(surface) => surface.secretTargetRegistryEntries ?? [],
|
||||
return listBootstrapChannelPlugins().flatMap(
|
||||
(plugin) => plugin.secrets?.secretTargetRegistryEntries ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
|
||||
type ChannelUnsupportedSecretRefSurface = {
|
||||
unsupportedSecretRefSurfacePatterns?: readonly string[];
|
||||
collectUnsupportedSecretRefConfigCandidates?: (
|
||||
raw: unknown,
|
||||
) => UnsupportedSecretRefConfigCandidate[];
|
||||
};
|
||||
|
||||
const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
"commands.ownerDisplaySecret",
|
||||
"hooks.token",
|
||||
@@ -17,42 +9,9 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
"auth-profiles.oauth.*",
|
||||
] as const;
|
||||
|
||||
type BundledChannelContractSurfacesModule = {
|
||||
getBundledChannelContractSurfaces?: () => unknown[];
|
||||
};
|
||||
|
||||
const CONTRACT_SURFACES_MODULE_PATH = fileURLToPath(
|
||||
new URL("../channels/plugins/contract-surfaces.js", import.meta.url),
|
||||
);
|
||||
let bundledChannelContractSurfacesModule: BundledChannelContractSurfacesModule | null | undefined;
|
||||
let bundledChannelContractSurfacesLoader: ReturnType<typeof createJiti> | undefined;
|
||||
|
||||
function loadBundledChannelContractSurfacesModule(): BundledChannelContractSurfacesModule | null {
|
||||
if (bundledChannelContractSurfacesModule !== undefined) {
|
||||
return bundledChannelContractSurfacesModule;
|
||||
}
|
||||
try {
|
||||
bundledChannelContractSurfacesLoader ??= createJiti(import.meta.url, { interopDefault: true });
|
||||
bundledChannelContractSurfacesModule = bundledChannelContractSurfacesLoader(
|
||||
CONTRACT_SURFACES_MODULE_PATH,
|
||||
) as BundledChannelContractSurfacesModule;
|
||||
} catch {
|
||||
bundledChannelContractSurfacesModule = null;
|
||||
}
|
||||
return bundledChannelContractSurfacesModule;
|
||||
}
|
||||
|
||||
function listChannelUnsupportedSecretRefSurfaces(): ChannelUnsupportedSecretRefSurface[] {
|
||||
const module = loadBundledChannelContractSurfacesModule();
|
||||
if (typeof module?.getBundledChannelContractSurfaces !== "function") {
|
||||
return [];
|
||||
}
|
||||
return module.getBundledChannelContractSurfaces() as ChannelUnsupportedSecretRefSurface[];
|
||||
}
|
||||
|
||||
function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
return listChannelUnsupportedSecretRefSurfaces().flatMap(
|
||||
(surface) => surface.unsupportedSecretRefSurfacePatterns ?? [],
|
||||
return listBootstrapChannelPlugins().flatMap(
|
||||
(plugin) => plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,8 +74,8 @@ export function collectUnsupportedSecretRefConfigCandidates(
|
||||
}
|
||||
|
||||
if (isRecord(raw.channels)) {
|
||||
for (const surface of listChannelUnsupportedSecretRefSurfaces()) {
|
||||
const channelCandidates = surface.collectUnsupportedSecretRefConfigCandidates?.(raw);
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
const channelCandidates = plugin.secrets?.collectUnsupportedSecretRefConfigCandidates?.(raw);
|
||||
if (!channelCandidates?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { parseAgentSessionKey } from "./session-key-utils.js";
|
||||
|
||||
export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown";
|
||||
|
||||
type LegacySessionChatTypeSurface = {
|
||||
deriveLegacySessionChatType?: (sessionKey: string) => "direct" | "group" | "channel" | undefined;
|
||||
};
|
||||
|
||||
function listLegacySessionChatTypeSurfaces(): LegacySessionChatTypeSurface[] {
|
||||
return getBundledChannelContractSurfaces() as LegacySessionChatTypeSurface[];
|
||||
}
|
||||
|
||||
function deriveBuiltInLegacySessionChatType(
|
||||
scopedSessionKey: string,
|
||||
): SessionKeyChatType | undefined {
|
||||
@@ -52,8 +44,8 @@ export function deriveSessionChatType(sessionKey: string | undefined | null): Se
|
||||
if (builtInLegacy) {
|
||||
return builtInLegacy;
|
||||
}
|
||||
for (const surface of listLegacySessionChatTypeSurfaces()) {
|
||||
const derived = surface.deriveLegacySessionChatType?.(scoped);
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
const derived = plugin.messaging?.deriveLegacySessionChatType?.(scoped);
|
||||
if (derived) {
|
||||
return derived;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user