mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 23:22:32 +00:00
refactor(extensions): split channel runtime helper seams
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
|
||||
|
||||
export const discordChannelConfigUiHints = {
|
||||
"": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
35
extensions/discord/src/recipient-resolution.ts
Normal file
35
extensions/discord/src/recipient-resolution.ts
Normal 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 };
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
44
extensions/discord/src/send-target-parsing.ts
Normal file
44
extensions/discord/src/send-target-parsing.ts
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
53
extensions/discord/src/target-parsing.ts
Normal file
53
extensions/discord/src/target-parsing.ts
Normal 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" });
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
|
||||
|
||||
export const slackChannelConfigUiHints = {
|
||||
"": {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
81
extensions/slack/src/target-parsing.ts
Normal file
81
extensions/slack/src/target-parsing.ts
Normal 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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
16
package.json
16
package.json
@@ -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"
|
||||
|
||||
@@ -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.") ||
|
||||
|
||||
@@ -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.") ||
|
||||
|
||||
@@ -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",
|
||||
|
||||
93
src/channels/chat-meta-shared.ts
Normal file
93
src/channels/chat-meta-shared.ts
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
src/plugin-sdk/messaging-targets.ts
Normal file
14
src/plugin-sdk/messaging-targets.ts
Normal 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";
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
normalizeAtHashSlug,
|
||||
normalizeHyphenSlug,
|
||||
normalizeStringEntries,
|
||||
normalizeStringEntriesLower,
|
||||
|
||||
4
src/plugin-sdk/target-resolver-runtime.ts
Normal file
4
src/plugin-sdk/target-resolver-runtime.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
buildUnresolvedTargetResults,
|
||||
resolveTargetsWithOptionalToken,
|
||||
} from "../channels/plugins/target-resolvers.js";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
32
src/plugin-sdk/telegram-command-ui.ts
Normal file
32
src/plugin-sdk/telegram-command-ui.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
32
src/plugins/runtime-state.ts
Normal file
32
src/plugins/runtime-state.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user