refactor(extensions): split channel runtime helper seams

This commit is contained in:
Peter Steinberger
2026-04-04 07:39:37 +01:00
parent 667a54a4b7
commit d5cb8cebcd
54 changed files with 685 additions and 412 deletions

View File

@@ -1,6 +1,6 @@
export { bluebubblesPlugin } from "./src/channel.js";
export * from "./src/conversation-id.js";
export * from "./src/conversation-bindings.js";
export { collectBlueBubblesStatusIssues } from "./src/status-issues.js";
export {
resolveBlueBubblesGroupRequireMention,
resolveBlueBubblesGroupToolPolicy,

View File

@@ -1,4 +1,4 @@
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { bluebubblesPlugin } from "./src/channel.js";
import { setBlueBubblesRuntime } from "./src/runtime.js";

View File

@@ -1,4 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { bluebubblesSetupPlugin } from "./src/channel.setup.js";
export { bluebubblesSetupPlugin } from "./src/channel.setup.js";

View File

@@ -1,4 +1,4 @@
export { resolveAckReaction } from "./runtime-api.js";
export { resolveAckReaction } from "openclaw/plugin-sdk/channel-feedback";
export { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
export { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
export { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";

View File

@@ -2,7 +2,7 @@ import {
buildChannelOutboundSessionRoute,
stripChannelTargetPrefix,
type ChannelOutboundSessionRouteParams,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/channel-core";
import { parseBlueBubblesTarget } from "./targets.js";
export function resolveBlueBubblesOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {

View File

@@ -1,4 +1,4 @@
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { discordPlugin } from "./src/channel.js";
import { setDiscordRuntime } from "./src/runtime.js";

View File

@@ -1,4 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { discordSetupPlugin } from "./src/channel.setup.js";
export { discordSetupPlugin } from "./src/channel.setup.js";

View File

@@ -6,6 +6,24 @@ export {
resolveConfiguredFromCredentialStatuses,
} from "openclaw/plugin-sdk/channel-status";
export { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
export { getChatChannelMeta } from "openclaw/plugin-sdk/core";
export type { ChannelPlugin } from "openclaw/plugin-sdk/core";
export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
const DISCORD_CHANNEL_META = {
id: "discord",
label: "Discord",
selectionLabel: "Discord (Bot API)",
detailLabel: "Discord Bot",
docsPath: "/channels/discord",
docsLabel: "discord",
blurb: "very well supported right now.",
systemImage: "bubble.left.and.bubble.right",
markdownCapable: true,
} as const;
export function getChatChannelMeta(id: string) {
if (id !== DISCORD_CHANNEL_META.id) {
throw new Error(`Unsupported Discord channel meta lookup: ${id}`);
}
return DISCORD_CHANNEL_META;
}

View File

@@ -1,4 +1,4 @@
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
export const discordChannelConfigUiHints = {
"": {

View File

@@ -5,7 +5,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import { resolveDiscordAccount } from "./accounts.js";
import { parseDiscordTarget } from "./targets.js";
import { parseDiscordTarget } from "./target-parsing.js";
function normalizeDiscordApproverId(value: string): string | undefined {
const trimmed = value.trim();

View File

@@ -4,7 +4,7 @@ import {
type GroupToolPolicyBySenderConfig,
type GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/channel-policy";
import { normalizeAtHashSlug } from "openclaw/plugin-sdk/core";
import { normalizeAtHashSlug } from "openclaw/plugin-sdk/string-normalization-runtime";
import type { DiscordConfig } from "./runtime-api.js";
function normalizeDiscordSlug(value?: string | null) {

View File

@@ -0,0 +1,88 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
BINDINGS_BY_THREAD_ID,
ensureBindingsLoaded,
resolveBindingIdsForSession,
saveBindingsToDisk,
setBindingRecord,
shouldPersistBindingMutations,
} from "./thread-bindings.state.js";
import type { ThreadBindingRecord, ThreadBindingTargetKind } from "./thread-bindings.types.js";
function normalizeNonNegativeMs(raw: number): number {
if (!Number.isFinite(raw)) {
return 0;
}
return Math.max(0, Math.floor(raw));
}
function resolveBindingIdsForTargetSession(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}) {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
return resolveBindingIdsForSession({
targetSessionKey,
accountId,
targetKind: params.targetKind,
});
}
function updateBindingsForTargetSession(
ids: string[],
update: (existing: ThreadBindingRecord, now: number) => ThreadBindingRecord,
) {
if (ids.length === 0) {
return [];
}
const now = Date.now();
const updated: ThreadBindingRecord[] = [];
for (const bindingKey of ids) {
const existing = BINDINGS_BY_THREAD_ID.get(bindingKey);
if (!existing) {
continue;
}
const nextRecord = update(existing, now);
setBindingRecord(nextRecord);
updated.push(nextRecord);
}
if (updated.length > 0 && shouldPersistBindingMutations()) {
saveBindingsToDisk({ force: true });
}
return updated;
}
export function setThreadBindingIdleTimeoutBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
idleTimeoutMs: number;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
const idleTimeoutMs = normalizeNonNegativeMs(params.idleTimeoutMs);
return updateBindingsForTargetSession(ids, (existing, now) => ({
...existing,
idleTimeoutMs,
lastActivityAt: now,
}));
}
export function setThreadBindingMaxAgeBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
maxAgeMs: number;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
const maxAgeMs = normalizeNonNegativeMs(params.maxAgeMs);
return updateBindingsForTargetSession(ids, (existing, now) => ({
...existing,
maxAgeMs,
boundAt: now,
lastActivityAt: now,
}));
}

View File

@@ -1,4 +1,4 @@
import { parseDiscordTarget } from "./targets.js";
import { parseDiscordTarget } from "./target-parsing.js";
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
// Default bare IDs to channels so routing is stable across tool actions.

View File

@@ -5,7 +5,7 @@ import {
type RoutePeer,
} from "openclaw/plugin-sdk/routing";
import type { OpenClawConfig } from "./runtime-api.js";
import { parseDiscordTarget } from "./targets.js";
import { parseDiscordTarget } from "./target-parsing.js";
export type ResolveDiscordOutboundSessionRouteParams = {
cfg: OpenClawConfig;

View File

@@ -0,0 +1,35 @@
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveDiscordAccount } from "./accounts.js";
import { parseAndResolveDiscordTarget } from "./target-resolver.js";
type DiscordRecipient =
| {
kind: "user";
id: string;
}
| {
kind: "channel";
id: string;
};
export async function parseAndResolveRecipient(
raw: string,
accountId?: string,
cfg?: OpenClawConfig,
): Promise<DiscordRecipient> {
const resolvedCfg = cfg ?? loadConfig();
const accountInfo = resolveDiscordAccount({ cfg: resolvedCfg, accountId });
const trimmed = raw.trim();
const parseOptions = {
ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
};
const resolved = await parseAndResolveDiscordTarget(
raw,
{
cfg: resolvedCfg,
accountId: accountInfo.accountId,
},
parseOptions,
);
return { kind: resolved.kind, id: resolved.id };
}

View File

@@ -11,7 +11,11 @@ export type {
ChannelMessageActionContext,
ChannelMessageActionName,
} from "openclaw/plugin-sdk/channel-contract";
export type { ChannelPlugin, OpenClawPluginApi, PluginRuntime } from "openclaw/plugin-sdk/core";
export type {
ChannelPlugin,
OpenClawPluginApi,
PluginRuntime,
} from "openclaw/plugin-sdk/channel-plugin-common";
export type {
DiscordAccountConfig,
DiscordActionConfig,
@@ -48,8 +52,7 @@ export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/acco
export {
emptyPluginConfigSchema,
formatPairingApproveHint,
getChatChannelMeta,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/channel-plugin-common";
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
export { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
export {
@@ -57,4 +60,5 @@ export {
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
export { getChatChannelMeta } from "./channel-api.js";
export { resolveDiscordOutboundSessionRoute } from "./outbound-session-route.js";

View File

@@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
type DiscordChannelRuntime = {

View File

@@ -0,0 +1,44 @@
import {
buildMessagingTarget,
type MessagingTarget,
type MessagingTargetParseOptions,
} from "openclaw/plugin-sdk/messaging-targets";
import { parseMentionPrefixOrAtUserTarget } from "openclaw/plugin-sdk/messaging-targets";
export type SendDiscordTarget = MessagingTarget;
export type SendDiscordTargetParseOptions = MessagingTargetParseOptions;
export function parseDiscordSendTarget(
raw: string,
options: SendDiscordTargetParseOptions = {},
): SendDiscordTarget | undefined {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const userTarget = parseMentionPrefixOrAtUserTarget({
raw: trimmed,
mentionPattern: /^<@!?(\d+)>$/,
prefixes: [
{ prefix: "user:", kind: "user" },
{ prefix: "channel:", kind: "channel" },
{ prefix: "discord:", kind: "user" },
],
atUserPattern: /^\d+$/,
atUserErrorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)",
});
if (userTarget) {
return userTarget;
}
if (/^\d+$/.test(trimmed)) {
if (options.defaultKind) {
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
}
throw new Error(
options.ambiguousMessage ??
`Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}

View File

@@ -18,6 +18,7 @@ import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
import { resolveDiscordAccount } from "./accounts.js";
import { resolveDiscordClientAccountContext } from "./client.js";
import { rewriteDiscordKnownMentions } from "./mentions.js";
import { parseAndResolveRecipient } from "./recipient-resolution.js";
import {
buildDiscordMessagePayload,
buildDiscordSendError,
@@ -25,7 +26,6 @@ import {
createDiscordClient,
normalizeDiscordPollInput,
normalizeStickerIds,
parseAndResolveRecipient,
resolveChannelId,
resolveDiscordChannelType,
resolveDiscordSendComponents,

View File

@@ -2,7 +2,6 @@ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import { inspectDiscordAccount } from "./account-inspect.js";
import {
listDiscordAccountIds,
@@ -53,7 +52,7 @@ export function createDiscordPluginBase(params: {
| "config"
| "setup"
> {
return createChannelPluginBase({
return {
id: DISCORD_CHANNEL,
setupWizard: discordSetupWizard,
meta: { ...getChatChannelMeta(DISCORD_CHANNEL) },
@@ -90,7 +89,7 @@ export function createDiscordPluginBase(params: {
}),
},
setup: params.setup,
}) as Pick<
} as Pick<
ChannelPlugin<ResolvedDiscordAccount>,
| "id"
| "meta"

View File

@@ -0,0 +1,53 @@
import {
buildMessagingTarget,
parseMentionPrefixOrAtUserTarget,
requireTargetKind,
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "openclaw/plugin-sdk/messaging-targets";
export type DiscordTargetKind = MessagingTargetKind;
export type DiscordTarget = MessagingTarget;
export type DiscordTargetParseOptions = MessagingTargetParseOptions;
export function parseDiscordTarget(
raw: string,
options: DiscordTargetParseOptions = {},
): DiscordTarget | undefined {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const userTarget = parseMentionPrefixOrAtUserTarget({
raw: trimmed,
mentionPattern: /^<@!?(\d+)>$/,
prefixes: [
{ prefix: "user:", kind: "user" },
{ prefix: "channel:", kind: "channel" },
{ prefix: "discord:", kind: "user" },
],
atUserPattern: /^\d+$/,
atUserErrorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)",
});
if (userTarget) {
return userTarget;
}
if (/^\d+$/.test(trimmed)) {
if (options.defaultKind) {
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
}
throw new Error(
options.ambiguousMessage ??
`Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}
export function resolveDiscordChannelId(raw: string): string {
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
}

View File

@@ -1,162 +1,12 @@
import {
buildMessagingTarget,
parseMentionPrefixOrAtUserTarget,
requireTargetKind,
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "openclaw/plugin-sdk/channel-targets";
import type { DirectoryConfigParams } from "openclaw/plugin-sdk/directory-runtime";
import { resolveDiscordAccount } from "./accounts.js";
import { rememberDiscordDirectoryUser } from "./directory-cache.js";
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
parseDiscordTarget,
type DiscordTarget,
type DiscordTargetKind,
type DiscordTargetParseOptions,
resolveDiscordChannelId,
} from "./target-parsing.js";
import { resolveDiscordTarget } from "./target-resolver.js";
export type DiscordTargetKind = MessagingTargetKind;
export type DiscordTarget = MessagingTarget;
type DiscordTargetParseOptions = MessagingTargetParseOptions;
export function parseDiscordTarget(
raw: string,
options: DiscordTargetParseOptions = {},
): DiscordTarget | undefined {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const userTarget = parseMentionPrefixOrAtUserTarget({
raw: trimmed,
mentionPattern: /^<@!?(\d+)>$/,
prefixes: [
{ prefix: "user:", kind: "user" },
{ prefix: "channel:", kind: "channel" },
{ prefix: "discord:", kind: "user" },
],
atUserPattern: /^\d+$/,
atUserErrorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)",
});
if (userTarget) {
return userTarget;
}
if (/^\d+$/.test(trimmed)) {
if (options.defaultKind) {
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
}
throw new Error(
options.ambiguousMessage ??
`Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}
export function resolveDiscordChannelId(raw: string): string {
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
}
/**
* Resolve a Discord username to user ID using the directory lookup.
* This enables sending DMs by username instead of requiring explicit user IDs.
*
* @param raw - The username or raw target string (e.g., "john.doe")
* @param options - Directory configuration params (cfg, accountId, limit)
* @param parseOptions - Messaging target parsing options (defaults, ambiguity message)
* @returns Parsed MessagingTarget with user ID, or undefined if not found
*/
export async function resolveDiscordTarget(
raw: string,
options: DirectoryConfigParams,
parseOptions: DiscordTargetParseOptions = {},
): Promise<MessagingTarget | undefined> {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const likelyUsername = isLikelyUsername(trimmed);
const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
// Parse directly if it's already a known format. Use a safe parse so ambiguous
// numeric targets don't throw when we still want to attempt username lookup.
const directParse = safeParseDiscordTarget(trimmed, parseOptions);
if (directParse && directParse.kind !== "channel" && !likelyUsername) {
return directParse;
}
if (!shouldLookup) {
return directParse ?? parseDiscordTarget(trimmed, parseOptions);
}
// Try to resolve as a username via directory lookup
try {
const directoryEntries = await listDiscordDirectoryPeersLive({
...options,
query: trimmed,
limit: 1,
});
const match = directoryEntries[0];
if (match && match.kind === "user") {
// Extract user ID from the directory entry (format: "user:<id>")
const userId = match.id.replace(/^user:/, "");
const resolvedAccountId = resolveDiscordAccount({
cfg: options.cfg,
accountId: options.accountId,
}).accountId;
rememberDiscordDirectoryUser({
accountId: resolvedAccountId,
userId,
handles: [trimmed, match.name, match.handle],
});
return buildMessagingTarget("user", userId, trimmed);
}
} catch {
// Directory lookup failed - fall through to parse as-is
// This preserves existing behavior for channel names
}
// Fallback to original parsing (for channels, etc.)
return parseDiscordTarget(trimmed, parseOptions);
}
function safeParseDiscordTarget(
input: string,
options: DiscordTargetParseOptions,
): MessagingTarget | undefined {
try {
return parseDiscordTarget(input, options);
} catch {
return undefined;
}
}
function isExplicitUserLookup(input: string, options: DiscordTargetParseOptions): boolean {
if (/^<@!?(\d+)>$/.test(input)) {
return true;
}
if (/^(user:|discord:)/.test(input)) {
return true;
}
if (input.startsWith("@")) {
return true;
}
if (/^\d+$/.test(input)) {
return options.defaultKind === "user";
}
return false;
}
/**
* Check if a string looks like a Discord username (not a mention, prefix, or ID).
* Usernames typically don't start with special characters except underscore.
*/
function isLikelyUsername(input: string): boolean {
// Skip if it's already a known format
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
return false;
}
// Likely a username if it doesn't match known patterns
return true;
}
export { parseDiscordTarget, resolveDiscordChannelId };
export type { DiscordTarget, DiscordTargetKind, DiscordTargetParseOptions };
export { resolveDiscordTarget };

View File

@@ -1,4 +1,4 @@
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { signalPlugin } from "./src/channel.js";
import { setSignalRuntime } from "./src/runtime.js";

View File

@@ -1,4 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { signalSetupPlugin } from "./src/channel.setup.js";
export { signalSetupPlugin } from "./src/channel.setup.js";

View File

@@ -1,4 +1,4 @@
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { slackPlugin } from "./src/channel.js";
import { registerSlackPluginHttpRoutes } from "./src/http/plugin-routes.js";
import { setSlackRuntime } from "./src/runtime.js";

View File

@@ -1,4 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { slackSetupPlugin } from "./src/channel.setup.js";
export { slackSetupPlugin } from "./src/channel.setup.js";

View File

@@ -4,7 +4,24 @@ export {
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
} from "openclaw/plugin-sdk/channel-status";
export type { ChannelPlugin } from "openclaw/plugin-sdk/core";
export { getChatChannelMeta } from "openclaw/plugin-sdk/core";
export type { OpenClawConfig } from "openclaw/plugin-sdk/core";
export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./targets.js";
export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./target-parsing.js";
const SLACK_CHANNEL_META = {
id: "slack",
label: "Slack",
selectionLabel: "Slack",
docsPath: "/channels/slack",
docsLabel: "slack",
blurb: "supports bot + app tokens, channels, threads, and interactive replies.",
systemImage: "number.square",
markdownCapable: true,
} as const;
export function getChatChannelMeta(id: string) {
if (id !== SLACK_CHANNEL_META.id) {
throw new Error(`Unsupported Slack channel meta lookup: ${id}`);
}
return SLACK_CHANNEL_META;
}

View File

@@ -1,4 +1,4 @@
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
export const slackChannelConfigUiHints = {
"": {

View File

@@ -5,7 +5,7 @@ import {
type GroupToolPolicyBySenderConfig,
type GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/channel-policy";
import { normalizeHyphenSlug } from "openclaw/plugin-sdk/core";
import { normalizeHyphenSlug } from "openclaw/plugin-sdk/string-normalization-runtime";
import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js";
type SlackChannelPolicyEntry = {

View File

@@ -9,18 +9,18 @@ export type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-co
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
export type {
ChannelPlugin,
OpenClawConfig,
OpenClawPluginApi,
PluginRuntime,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/channel-plugin-common";
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
export type { SlackAccountConfig } from "openclaw/plugin-sdk/config-runtime";
export {
emptyPluginConfigSchema,
formatPairingApproveHint,
getChatChannelMeta,
} from "openclaw/plugin-sdk/core";
} from "openclaw/plugin-sdk/channel-plugin-common";
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./targets.js";
export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./target-parsing.js";
export { getChatChannelMeta } from "./channel-api.js";
export {
createActionGate,
imageResultFromFile,

View File

@@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
type SlackChannelRuntime = {

View File

@@ -4,7 +4,6 @@ import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
formatDocsLink,
hasConfiguredSecretInput,
@@ -178,7 +177,7 @@ export function createSlackPluginBase(params: {
| "config"
| "setup"
> {
return createChannelPluginBase({
return {
id: SLACK_CHANNEL,
meta: {
...getChatChannelMeta(SLACK_CHANNEL),
@@ -240,7 +239,7 @@ export function createSlackPluginBase(params: {
}),
},
setup: params.setup,
}) as Pick<
} as Pick<
ChannelPlugin<ResolvedSlackAccount>,
| "id"
| "meta"

View File

@@ -0,0 +1,81 @@
import {
buildMessagingTarget,
ensureTargetId,
parseMentionPrefixOrAtUserTarget,
requireTargetKind,
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "openclaw/plugin-sdk/messaging-targets";
export type SlackTargetKind = MessagingTargetKind;
export type SlackTarget = MessagingTarget;
export type SlackTargetParseOptions = MessagingTargetParseOptions;
export function parseSlackTarget(
raw: string,
options: SlackTargetParseOptions = {},
): SlackTarget | undefined {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const userTarget = parseMentionPrefixOrAtUserTarget({
raw: trimmed,
mentionPattern: /^<@([A-Z0-9]+)>$/i,
prefixes: [
{ prefix: "user:", kind: "user" },
{ prefix: "channel:", kind: "channel" },
{ prefix: "slack:", kind: "user" },
],
atUserPattern: /^[A-Z0-9]+$/i,
atUserErrorMessage: "Slack DMs require a user id (use user:<id> or <@id>)",
});
if (userTarget) {
return userTarget;
}
if (trimmed.startsWith("#")) {
const candidate = trimmed.slice(1).trim();
const id = ensureTargetId({
candidate,
pattern: /^[A-Z0-9]+$/i,
errorMessage: "Slack channels require a channel id (use channel:<id>)",
});
return buildMessagingTarget("channel", id, trimmed);
}
if (options.defaultKind) {
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}
export function resolveSlackChannelId(raw: string): string {
const target = parseSlackTarget(raw, { defaultKind: "channel" });
return requireTargetKind({ platform: "Slack", target, kind: "channel" });
}
export function normalizeSlackMessagingTarget(raw: string): string | undefined {
return parseSlackTarget(raw, { defaultKind: "channel" })?.normalized;
}
export function looksLikeSlackTargetId(raw: string): boolean {
const trimmed = raw.trim();
if (!trimmed) {
return false;
}
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) {
return true;
}
if (/^(user|channel):/i.test(trimmed)) {
return true;
}
if (/^slack:/i.test(trimmed)) {
return true;
}
if (/^[@#]/.test(trimmed)) {
return true;
}
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
}

View File

@@ -1,81 +1,7 @@
import {
buildMessagingTarget,
ensureTargetId,
parseMentionPrefixOrAtUserTarget,
requireTargetKind,
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "openclaw/plugin-sdk/channel-targets";
export type SlackTargetKind = MessagingTargetKind;
export type SlackTarget = MessagingTarget;
type SlackTargetParseOptions = MessagingTargetParseOptions;
export function parseSlackTarget(
raw: string,
options: SlackTargetParseOptions = {},
): SlackTarget | undefined {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const userTarget = parseMentionPrefixOrAtUserTarget({
raw: trimmed,
mentionPattern: /^<@([A-Z0-9]+)>$/i,
prefixes: [
{ prefix: "user:", kind: "user" },
{ prefix: "channel:", kind: "channel" },
{ prefix: "slack:", kind: "user" },
],
atUserPattern: /^[A-Z0-9]+$/i,
atUserErrorMessage: "Slack DMs require a user id (use user:<id> or <@id>)",
});
if (userTarget) {
return userTarget;
}
if (trimmed.startsWith("#")) {
const candidate = trimmed.slice(1).trim();
const id = ensureTargetId({
candidate,
pattern: /^[A-Z0-9]+$/i,
errorMessage: "Slack channels require a channel id (use channel:<id>)",
});
return buildMessagingTarget("channel", id, trimmed);
}
if (options.defaultKind) {
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}
export function resolveSlackChannelId(raw: string): string {
const target = parseSlackTarget(raw, { defaultKind: "channel" });
return requireTargetKind({ platform: "Slack", target, kind: "channel" });
}
export function normalizeSlackMessagingTarget(raw: string): string | undefined {
return parseSlackTarget(raw, { defaultKind: "channel" })?.normalized;
}
export function looksLikeSlackTargetId(raw: string): boolean {
const trimmed = raw.trim();
if (!trimmed) {
return false;
}
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) {
return true;
}
if (/^(user|channel):/i.test(trimmed)) {
return true;
}
if (/^slack:/i.test(trimmed)) {
return true;
}
if (/^[@#]/.test(trimmed)) {
return true;
}
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
}
export {
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
parseSlackTarget,
resolveSlackChannelId,
} from "./target-parsing.js";
export type { SlackTarget, SlackTargetKind, SlackTargetParseOptions } from "./target-parsing.js";

View File

@@ -447,6 +447,14 @@
"types": "./dist/plugin-sdk/channel-actions.d.ts",
"default": "./dist/plugin-sdk/channel-actions.js"
},
"./plugin-sdk/channel-plugin-common": {
"types": "./dist/plugin-sdk/channel-plugin-common.d.ts",
"default": "./dist/plugin-sdk/channel-plugin-common.js"
},
"./plugin-sdk/channel-core": {
"types": "./dist/plugin-sdk/channel-core.d.ts",
"default": "./dist/plugin-sdk/channel-core.js"
},
"./plugin-sdk/channel-contract": {
"types": "./dist/plugin-sdk/channel-contract.d.ts",
"default": "./dist/plugin-sdk/channel-contract.js"
@@ -479,6 +487,10 @@
"types": "./dist/plugin-sdk/channel-targets.d.ts",
"default": "./dist/plugin-sdk/channel-targets.js"
},
"./plugin-sdk/messaging-targets": {
"types": "./dist/plugin-sdk/messaging-targets.d.ts",
"default": "./dist/plugin-sdk/messaging-targets.js"
},
"./plugin-sdk/feishu": {
"types": "./dist/plugin-sdk/feishu.d.ts",
"default": "./dist/plugin-sdk/feishu.js"
@@ -883,6 +895,10 @@
"types": "./dist/plugin-sdk/synthetic.d.ts",
"default": "./dist/plugin-sdk/synthetic.js"
},
"./plugin-sdk/target-resolver-runtime": {
"types": "./dist/plugin-sdk/target-resolver-runtime.d.ts",
"default": "./dist/plugin-sdk/target-resolver-runtime.js"
},
"./plugin-sdk/thread-ownership": {
"types": "./dist/plugin-sdk/thread-ownership.d.ts",
"default": "./dist/plugin-sdk/thread-ownership.js"

View File

@@ -44,6 +44,7 @@ function collectTopLevelPublicSurfaceEntries(pluginDir) {
const normalizedName = dirent.name.toLowerCase();
if (
/^config-api\.(?:[cm]?[jt]s)$/u.test(normalizedName) ||
normalizedName.includes(".test.") ||
normalizedName.includes(".spec.") ||
normalizedName.includes(".fixture.") ||

View File

@@ -70,6 +70,7 @@ function collectTopLevelPublicSurfaceEntries(pluginDir) {
const normalizedName = dirent.name.toLowerCase();
if (
normalizedName.endsWith(".d.ts") ||
/^config-api\.(?:[cm]?[jt]s)$/u.test(normalizedName) ||
normalizedName.includes(".test.") ||
normalizedName.includes(".spec.") ||
normalizedName.includes(".fixture.") ||

View File

@@ -101,6 +101,8 @@
"channel-config-primitives",
"channel-config-schema",
"channel-actions",
"channel-plugin-common",
"channel-core",
"channel-contract",
"channel-feedback",
"channel-inbound",
@@ -130,6 +132,7 @@
"realtime-transcription",
"realtime-voice",
"media-understanding",
"messaging-targets",
"request-url",
"runtime-store",
"json-store",
@@ -210,6 +213,7 @@
"string-normalization-runtime",
"state-paths",
"synthetic",
"target-resolver-runtime",
"thread-ownership",
"tlon",
"tool-send",

View File

@@ -0,0 +1,93 @@
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
import type { PluginPackageChannel } from "../plugins/manifest.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js";
import type { ChannelMeta } from "./plugins/types.js";
export type ChatChannelMeta = ChannelMeta;
const CHAT_CHANNEL_ID_SET = new Set<string>(CHAT_CHANNEL_ORDER);
function toChatChannelMeta(params: {
id: ChatChannelId;
channel: PluginPackageChannel;
}): ChatChannelMeta {
const label = params.channel.label?.trim();
if (!label) {
throw new Error(`Missing label for bundled chat channel "${params.id}"`);
}
return {
id: params.id,
label,
selectionLabel: params.channel.selectionLabel?.trim() || label,
docsPath: params.channel.docsPath?.trim() || `/channels/${params.id}`,
docsLabel: params.channel.docsLabel?.trim() || undefined,
blurb: params.channel.blurb?.trim() || "",
...(params.channel.aliases?.length ? { aliases: params.channel.aliases } : {}),
...(params.channel.order !== undefined ? { order: params.channel.order } : {}),
...(params.channel.selectionDocsPrefix !== undefined
? { selectionDocsPrefix: params.channel.selectionDocsPrefix }
: {}),
...(params.channel.selectionDocsOmitLabel !== undefined
? { selectionDocsOmitLabel: params.channel.selectionDocsOmitLabel }
: {}),
...(params.channel.selectionExtras?.length
? { selectionExtras: params.channel.selectionExtras }
: {}),
...(params.channel.detailLabel?.trim()
? { detailLabel: params.channel.detailLabel.trim() }
: {}),
...(params.channel.systemImage?.trim()
? { systemImage: params.channel.systemImage.trim() }
: {}),
...(params.channel.markdownCapable !== undefined
? { markdownCapable: params.channel.markdownCapable }
: {}),
...(params.channel.showConfigured !== undefined
? { showConfigured: params.channel.showConfigured }
: {}),
...(params.channel.quickstartAllowFrom !== undefined
? { quickstartAllowFrom: params.channel.quickstartAllowFrom }
: {}),
...(params.channel.forceAccountBinding !== undefined
? { forceAccountBinding: params.channel.forceAccountBinding }
: {}),
...(params.channel.preferSessionLookupForAnnounceTarget !== undefined
? {
preferSessionLookupForAnnounceTarget: params.channel.preferSessionLookupForAnnounceTarget,
}
: {}),
...(params.channel.preferOver?.length ? { preferOver: params.channel.preferOver } : {}),
};
}
export function buildChatChannelMetaById(): Record<ChatChannelId, ChatChannelMeta> {
const entries = new Map<ChatChannelId, ChatChannelMeta>();
for (const entry of listBundledPluginMetadata({
includeChannelConfigs: true,
includeSyntheticChannelConfigs: false,
})) {
const channel =
entry.packageManifest && "channel" in entry.packageManifest
? entry.packageManifest.channel
: undefined;
if (!channel) {
continue;
}
const rawId = channel?.id?.trim();
if (!rawId || !CHAT_CHANNEL_ID_SET.has(rawId)) {
continue;
}
const id = rawId;
entries.set(
id,
toChatChannelMeta({
id,
channel,
}),
);
}
return Object.freeze(Object.fromEntries(entries)) as Record<ChatChannelId, ChatChannelMeta>;
}

View File

@@ -1,61 +0,0 @@
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
export type TelegramCustomCommandInput = {
command?: string | null;
description?: string | null;
};
export type TelegramCustomCommandIssue = {
index: number;
field: "command" | "description";
message: string;
};
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[];
};
};
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
pluginId: "telegram",
preferredBasename: "contract-surfaces.ts",
});
if (!contract) {
throw new Error("telegram command config contract surface is unavailable");
}
return contract;
}
export const TELEGRAM_COMMAND_NAME_PATTERN =
loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
export function normalizeTelegramCommandName(value: string): string {
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);
}
export function normalizeTelegramCommandDescription(value: string): string {
return loadTelegramCommandConfigContract().normalizeTelegramCommandDescription(value);
}
export function resolveTelegramCustomCommands(params: {
commands?: TelegramCustomCommandInput[] | null;
reservedCommands?: Set<string>;
checkReserved?: boolean;
checkDuplicates?: boolean;
}): {
commands: Array<{ command: string; description: string }>;
issues: TelegramCustomCommandIssue[];
} {
return loadTelegramCommandConfigContract().resolveTelegramCustomCommands(params);
}

View File

@@ -5,7 +5,7 @@ import {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
} from "./telegram-command-config.js";
} from "../plugin-sdk/telegram-command-config.js";
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
import {
ChannelHealthMonitorSchema,

View File

@@ -5,7 +5,7 @@ import { normalizeAnyChannelId } from "../../channels/registry.js";
import { resolveStateDir } from "../../config/paths.js";
import { loadJsonFile } from "../../infra/json-file.js";
import { writeJsonFileAtomically } from "../../plugin-sdk/json-store.js";
import { getActivePluginChannelRegistry } from "../../plugins/runtime.js";
import { getActivePluginChannelRegistryFromState } from "../../plugins/runtime-state.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import type {
ConversationRef,
@@ -127,9 +127,10 @@ function resolveChannelSupportsCurrentConversationBinding(channel: string): bool
const matchesPluginId = (plugin: { id: string; meta?: { aliases?: readonly string[] } }) =>
plugin.id === normalized ||
(plugin.meta?.aliases ?? []).some((alias) => alias.trim().toLowerCase() === normalized);
// Keep this resolver on the active runtime registry only. Importing bundled
// channel loaders here creates a module cycle through plugin-sdk surfaces.
const plugin = getActivePluginChannelRegistry()?.channels.find((entry) =>
// Read the already-installed runtime channel registry from shared state only.
// Importing plugins/runtime here creates a module cycle through plugin-sdk
// surfaces during bundled channel discovery.
const plugin = getActivePluginChannelRegistryFromState()?.channels.find((entry) =>
matchesPluginId(entry.plugin),
)?.plugin;
if (plugin?.conversationBindings?.supportsCurrentConversationBinding === true) {

View File

@@ -1,6 +1,6 @@
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js";
export { buildCommandsPaginationKeyboard } from "./telegram-command-ui.js";
export {
createPreCryptoDirectDmAuthorizer,
resolveInboundDirectDmAccessWithRuntime,
@@ -86,37 +86,6 @@ export {
buildHelpMessage,
} from "../auto-reply/status.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,
);
}
export type ResolveSenderCommandAuthorizationParams = {
cfg: OpenClawConfig;
rawBody: string;

View File

@@ -0,0 +1,14 @@
export {
buildMessagingTarget,
ensureTargetId,
normalizeTargetId,
parseAtUserTarget,
parseMentionPrefixOrAtUserTarget,
parseTargetMention,
parseTargetPrefix,
parseTargetPrefixes,
requireTargetKind,
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "../channels/targets.js";

View File

@@ -1,4 +1,5 @@
export {
normalizeAtHashSlug,
normalizeHyphenSlug,
normalizeStringEntries,
normalizeStringEntriesLower,

View File

@@ -0,0 +1,4 @@
export {
buildUnresolvedTargetResults,
resolveTargetsWithOptionalToken,
} from "../channels/plugins/target-resolvers.js";

View File

@@ -1,8 +1,61 @@
export {
TELEGRAM_COMMAND_NAME_PATTERN,
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
type TelegramCustomCommandInput,
type TelegramCustomCommandIssue,
} from "../config/telegram-command-config.js";
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
export type TelegramCustomCommandInput = {
command?: string | null;
description?: string | null;
};
export type TelegramCustomCommandIssue = {
index: number;
field: "command" | "description";
message: string;
};
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[];
};
};
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
pluginId: "telegram",
preferredBasename: "contract-surfaces.ts",
});
if (!contract) {
throw new Error("telegram command config contract surface is unavailable");
}
return contract;
}
export const TELEGRAM_COMMAND_NAME_PATTERN =
loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
export function normalizeTelegramCommandName(value: string): string {
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);
}
export function normalizeTelegramCommandDescription(value: string): string {
return loadTelegramCommandConfigContract().normalizeTelegramCommandDescription(value);
}
export function resolveTelegramCustomCommands(params: {
commands?: TelegramCustomCommandInput[] | null;
reservedCommands?: Set<string>;
checkReserved?: boolean;
checkDuplicates?: boolean;
}): {
commands: Array<{ command: string; description: string }>;
issues: TelegramCustomCommandIssue[];
} {
return loadTelegramCommandConfigContract().resolveTelegramCustomCommands(params);
}

View File

@@ -0,0 +1,32 @@
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,
);
}

View File

@@ -2,8 +2,13 @@
// expiry and session-binding record types without loading the full
// conversation-runtime surface.
export { resolveThreadBindingConversationIdFromBindingId } from "../channels/thread-binding-id.js";
export { resolveThreadBindingFarewellText } from "../channels/thread-bindings-messages.js";
export { resolveThreadBindingLifecycle } from "../channels/thread-bindings-policy.js";
export {
resolveThreadBindingIdleTimeoutMsForChannel,
resolveThreadBindingLifecycle,
resolveThreadBindingMaxAgeMsForChannel,
} from "../channels/thread-bindings-policy.js";
export type {
BindingTargetKind,
SessionBindingAdapter,

View File

@@ -150,6 +150,9 @@ function isTopLevelPublicSurfaceSource(name: string): boolean {
if (name.endsWith(".d.ts")) {
return false;
}
if (/^config-api(\.[cm]?[jt]s)$/u.test(name)) {
return false;
}
return !/(\.test|\.spec)(\.[cm]?[jt]s)$/u.test(name);
}

View File

@@ -0,0 +1,32 @@
import type { PluginRegistry } from "./registry.js";
export const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
export type RegistrySurfaceState = {
registry: PluginRegistry | null;
pinned: boolean;
version: number;
};
export type RegistryState = {
activeRegistry: PluginRegistry | null;
activeVersion: number;
httpRoute: RegistrySurfaceState;
channel: RegistrySurfaceState;
key: string | null;
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable";
importedPluginIds: Set<string>;
};
type GlobalRegistryState = typeof globalThis & {
[PLUGIN_REGISTRY_STATE]?: RegistryState;
};
export function getPluginRegistryState(): RegistryState | undefined {
return (globalThis as GlobalRegistryState)[PLUGIN_REGISTRY_STATE];
}
export function getActivePluginChannelRegistryFromState(): PluginRegistry | null {
const state = getPluginRegistryState();
return state?.channel.registry ?? state?.activeRegistry ?? null;
}

View File

@@ -1,30 +1,17 @@
import { createEmptyPluginRegistry } from "./registry-empty.js";
import type { PluginRegistry } from "./registry.js";
const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
type RegistrySurfaceState = {
registry: PluginRegistry | null;
pinned: boolean;
version: number;
};
type RegistryState = {
activeRegistry: PluginRegistry | null;
activeVersion: number;
httpRoute: RegistrySurfaceState;
channel: RegistrySurfaceState;
key: string | null;
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable";
importedPluginIds: Set<string>;
};
import {
PLUGIN_REGISTRY_STATE,
type RegistryState,
type RegistrySurfaceState,
} from "./runtime-state.js";
const state: RegistryState = (() => {
const globalState = globalThis as typeof globalThis & {
[REGISTRY_STATE]?: RegistryState;
[PLUGIN_REGISTRY_STATE]?: RegistryState;
};
if (!globalState[REGISTRY_STATE]) {
globalState[REGISTRY_STATE] = {
if (!globalState[PLUGIN_REGISTRY_STATE]) {
globalState[PLUGIN_REGISTRY_STATE] = {
activeRegistry: null,
activeVersion: 0,
httpRoute: {
@@ -42,7 +29,7 @@ const state: RegistryState = (() => {
importedPluginIds: new Set<string>(),
};
}
return globalState[REGISTRY_STATE];
return globalState[PLUGIN_REGISTRY_STATE];
})();
export function recordImportedPluginId(pluginId: string): void {

View File

@@ -47,6 +47,10 @@ const CORE_SECRET_SURFACE_GUARDS = [
/\bWhatsAppConfigSchema\b/,
],
},
{
path: "src/plugin-sdk/command-auth.ts",
forbiddenPatterns: [/\bpluginId:\s*"telegram"/],
},
] as const;
describe("channel secret contract surface guardrails", () => {

View File

@@ -41,7 +41,7 @@ function getBluebubblesPlugin(): ChannelPlugin {
if (!bluebubblesPluginCache) {
({ bluebubblesPlugin: bluebubblesPluginCache } = loadBundledPluginPublicSurfaceSync<{
bluebubblesPlugin: ChannelPlugin;
}>({ pluginId: "bluebubbles", artifactBasename: "api.js" }));
}>({ pluginId: "bluebubbles", artifactBasename: "index.js" }));
}
return bluebubblesPluginCache;
}