mirror of https://github.com/openclaw/openclaw.git
refactor(plugins): narrow bundled channel core seams
This commit is contained in:
parent
381ee4d218
commit
667a54a4b7
|
|
@ -1,6 +1,5 @@
|
|||
export {
|
||||
BLUEBUBBLES_ACTION_NAMES,
|
||||
BLUEBUBBLES_ACTIONS,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
} from "./runtime-api.js";
|
||||
export { BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_ACTIONS } from "./actions-contract.js";
|
||||
export type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
export const BLUEBUBBLES_ACTIONS = {
|
||||
react: { gate: "reactions" },
|
||||
edit: { gate: "edit", unsupportedOnMacOS26: true },
|
||||
unsend: { gate: "unsend" },
|
||||
reply: { gate: "reply" },
|
||||
sendWithEffect: { gate: "sendWithEffect" },
|
||||
renameGroup: { gate: "renameGroup", groupOnly: true },
|
||||
setGroupIcon: { gate: "setGroupIcon", groupOnly: true },
|
||||
addParticipant: { gate: "addParticipant", groupOnly: true },
|
||||
removeParticipant: { gate: "removeParticipant", groupOnly: true },
|
||||
leaveGroup: { gate: "leaveGroup", groupOnly: true },
|
||||
sendAttachment: { gate: "sendAttachment" },
|
||||
} as const;
|
||||
|
||||
type BlueBubblesActionSpecs = typeof BLUEBUBBLES_ACTIONS;
|
||||
|
||||
export const BLUEBUBBLES_ACTION_NAMES = Object.keys(BLUEBUBBLES_ACTIONS) as Array<
|
||||
keyof BlueBubblesActionSpecs
|
||||
>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import {
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
buildProbeChannelStatusSummary,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import {
|
||||
createComputedAccountStatusAdapter,
|
||||
|
|
@ -48,6 +47,7 @@ import type { ChannelAccountSnapshot, ChannelPlugin } from "./runtime-api.js";
|
|||
import { resolveBlueBubblesOutboundSessionRoute } from "./session-route.js";
|
||||
import { blueBubblesSetupAdapter } from "./setup-core.js";
|
||||
import { blueBubblesSetupWizard } from "./setup-surface.js";
|
||||
import { collectBlueBubblesStatusIssues } from "./status-issues.js";
|
||||
import {
|
||||
extractHandleFromChatGuid,
|
||||
inferBlueBubblesTargetChatType,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
registerSessionBindingAdapter,
|
||||
resolveThreadBindingConversationIdFromBindingId,
|
||||
|
|
@ -8,8 +9,7 @@ import {
|
|||
type BindingTargetKind,
|
||||
type SessionBindingAdapter,
|
||||
type SessionBindingRecord,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
} from "openclaw/plugin-sdk/thread-bindings-runtime";
|
||||
|
||||
type BlueBubblesBindingTargetKind = "subagent" | "acp";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,42 +5,45 @@ export {
|
|||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
export type { HistoryEntry } from "openclaw/plugin-sdk/bluebubbles";
|
||||
} from "openclaw/plugin-sdk/channel-actions";
|
||||
export type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
export {
|
||||
evictOldHistoryKeys,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { resolveControlCommandGate } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { logAckFailure, logInboundDrop, logTypingFailure } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_ACTIONS } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles";
|
||||
} from "openclaw/plugin-sdk/reply-history";
|
||||
export { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
export { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
||||
export { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_ACTIONS } from "./actions-contract.js";
|
||||
export { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";
|
||||
export { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
|
||||
export { collectBlueBubblesStatusIssues } from "./status-issues.js";
|
||||
export type {
|
||||
BaseProbeResult,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
export type { ChannelPlugin } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { parseFiniteNumber } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/bluebubbles";
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
export type {
|
||||
ChannelPlugin,
|
||||
OpenClawConfig,
|
||||
PluginRuntime,
|
||||
} from "openclaw/plugin-sdk/channel-core";
|
||||
export { parseFiniteNumber } from "openclaw/plugin-sdk/infra-runtime";
|
||||
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
export {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { readBooleanParam } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { mapAllowFromEntries } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { resolveRequestUrl } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { buildProbeChannelStatusSummary } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles";
|
||||
export { extractToolSend } from "openclaw/plugin-sdk/bluebubbles";
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
export { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
export { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
|
||||
export { buildProbeChannelStatusSummary } from "openclaw/plugin-sdk/channel-status";
|
||||
export { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
|
||||
export { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
export {
|
||||
WEBHOOK_RATE_LIMIT_DEFAULTS,
|
||||
createFixedWindowRateLimiter,
|
||||
|
|
@ -50,7 +53,7 @@ export {
|
|||
resolveRequestClientIp,
|
||||
resolveWebhookTargetWithAuthOrRejectSync,
|
||||
withResolvedWebhookRequestPipeline,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
export {
|
||||
evaluateSupplementalContextVisibility,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectBlueBubblesStatusIssues } from "./status-issues.js";
|
||||
|
||||
describe("collectBlueBubblesStatusIssues", () => {
|
||||
it("reports unconfigured enabled accounts", () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import { collectIssuesForEnabledAccounts } from "openclaw/plugin-sdk/status-helpers";
|
||||
import type { ChannelAccountSnapshot } from "./runtime-api.js";
|
||||
|
||||
type BlueBubblesAccountStatus = {
|
||||
accountId?: unknown;
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
running?: unknown;
|
||||
baseUrl?: unknown;
|
||||
lastError?: unknown;
|
||||
probe?: unknown;
|
||||
};
|
||||
|
||||
type BlueBubblesProbeResult = {
|
||||
ok?: boolean;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
baseUrl: value.baseUrl,
|
||||
lastError: value.lastError,
|
||||
probe: value.probe,
|
||||
};
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
error: asString(value.error) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectBlueBubblesStatusIssues(accounts: ChannelAccountSnapshot[]) {
|
||||
return collectIssuesForEnabledAccounts({
|
||||
accounts,
|
||||
readAccount: readBlueBubblesAccountStatus,
|
||||
collectIssues: ({ account, accountId, issues }) => {
|
||||
const configured = account.configured === true;
|
||||
const running = account.running === true;
|
||||
const lastError = asString(account.lastError);
|
||||
const probe = readBlueBubblesProbeResult(account.probe);
|
||||
|
||||
if (!configured) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Not configured (missing serverUrl or password).",
|
||||
fix: "Run: openclaw channels add bluebubbles --http-url <server-url> --password <password>",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (probe && probe.ok === false) {
|
||||
const errorDetail = probe.error
|
||||
? `: ${probe.error}`
|
||||
: probe.status
|
||||
? ` (HTTP ${probe.status})`
|
||||
: "";
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `BlueBubbles server unreachable${errorDetail}`,
|
||||
fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.",
|
||||
});
|
||||
}
|
||||
|
||||
if (running && lastError) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `Channel error: ${lastError}`,
|
||||
fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -9,10 +9,9 @@ import type {
|
|||
ChannelMessageActionAdapter,
|
||||
ChannelMessageToolDiscovery,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/channel-targets";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createChannelDirectoryAdapter,
|
||||
createRuntimeDirectoryLiveAdapter,
|
||||
|
|
@ -28,6 +27,7 @@ import {
|
|||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/target-resolver-runtime";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDiscordAccount,
|
||||
|
|
@ -51,10 +51,9 @@ import {
|
|||
resolveDiscordGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import {
|
||||
createThreadBindingManager,
|
||||
setThreadBindingIdleTimeoutBySessionKey,
|
||||
setThreadBindingMaxAgeBySessionKey,
|
||||
} from "./monitor/thread-bindings.js";
|
||||
} from "./monitor/thread-bindings.session-updates.js";
|
||||
import {
|
||||
looksLikeDiscordTargetId,
|
||||
normalizeDiscordMessagingTarget,
|
||||
|
|
@ -67,7 +66,7 @@ import { normalizeExplicitDiscordSessionKey } from "./session-key-normalization.
|
|||
import { discordSetupAdapter } from "./setup-core.js";
|
||||
import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues.js";
|
||||
import { parseDiscordTarget } from "./targets.js";
|
||||
import { parseDiscordTarget } from "./target-parsing.js";
|
||||
import { DiscordUiContainer } from "./ui.js";
|
||||
|
||||
type DiscordSendFn = typeof import("./send.js").sendMessageDiscord;
|
||||
|
|
@ -94,6 +93,9 @@ const loadDiscordResolveChannelsModule = createLazyRuntimeModule(
|
|||
() => import("./resolve-channels.js"),
|
||||
);
|
||||
const loadDiscordResolveUsersModule = createLazyRuntimeModule(() => import("./resolve-users.js"));
|
||||
const loadDiscordThreadBindingsManagerModule = createLazyRuntimeModule(
|
||||
() => import("./monitor/thread-bindings.manager.js"),
|
||||
);
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
|
|
@ -546,8 +548,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
|||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
defaultTopLevelPlacement: "child",
|
||||
createManager: ({ cfg, accountId }) =>
|
||||
createThreadBindingManager({
|
||||
createManager: async ({ cfg, accountId }) =>
|
||||
(await loadDiscordThreadBindingsManagerModule()).createThreadBindingManager({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
persist: false,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import {
|
|||
type DiscordComponentBuildResult,
|
||||
type DiscordComponentMessageSpec,
|
||||
} from "./components.js";
|
||||
import { parseAndResolveRecipient } from "./recipient-resolution.js";
|
||||
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
|
||||
import { sendMessageDiscord } from "./send.outbound.js";
|
||||
import {
|
||||
buildDiscordSendError,
|
||||
createDiscordClient,
|
||||
parseAndResolveRecipient,
|
||||
resolveChannelId,
|
||||
resolveDiscordChannelType,
|
||||
toDiscordFileBlob,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
import { PollLayoutType } from "discord-api-types/payloads/v10";
|
||||
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
|
||||
import { Routes, type APIChannel, type APIEmbed } from "discord-api-types/v10";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { extensionForMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
|
|
@ -21,12 +20,10 @@ import type { ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
|
|||
import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
|
||||
import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
|
||||
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { chunkDiscordTextWithMode } from "./chunk.js";
|
||||
import { createDiscordClient, resolveDiscordRest } from "./client.js";
|
||||
import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
|
||||
import { DiscordSendError } from "./send.types.js";
|
||||
import { parseDiscordTarget, resolveDiscordTarget } from "./targets.js";
|
||||
|
||||
const DISCORD_TEXT_LIMIT = 2000;
|
||||
const DISCORD_MAX_STICKERS = 3;
|
||||
|
|
@ -40,7 +37,6 @@ type DiscordRequest = RetryRunner;
|
|||
export type DiscordSendComponentFactory = (text: string) => TopLevelComponents[];
|
||||
export type DiscordSendComponents = TopLevelComponents[] | DiscordSendComponentFactory;
|
||||
export type DiscordSendEmbeds = Array<APIEmbed | Embed>;
|
||||
|
||||
type DiscordRecipient =
|
||||
| {
|
||||
kind: "user";
|
||||
|
|
@ -63,63 +59,6 @@ function normalizeReactionEmoji(raw: string) {
|
|||
return encodeURIComponent(identifier);
|
||||
}
|
||||
|
||||
function parseRecipient(raw: string): DiscordRecipient {
|
||||
const target = parseDiscordTarget(raw, {
|
||||
defaultKind: "channel",
|
||||
ambiguousMessage: `Ambiguous Discord recipient "${raw.trim()}". Use "user:${raw.trim()}" for DMs or "channel:${raw.trim()}" for channel messages.`,
|
||||
});
|
||||
if (!target) {
|
||||
throw new Error("Recipient is required for Discord sends");
|
||||
}
|
||||
return { kind: target.kind, id: target.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and resolve Discord recipient, including username lookup.
|
||||
* This enables sending DMs by username (e.g., "john.doe") by querying
|
||||
* the Discord directory to resolve usernames to user IDs.
|
||||
*
|
||||
* @param raw - The recipient string (username, ID, or known format)
|
||||
* @param accountId - Discord account ID to use for directory lookup
|
||||
* @returns Parsed DiscordRecipient with resolved user ID if applicable
|
||||
*/
|
||||
export async function parseAndResolveRecipient(
|
||||
raw: string,
|
||||
accountId?: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): Promise<DiscordRecipient> {
|
||||
const resolvedCfg = cfg ?? loadConfig();
|
||||
const accountInfo = resolveDiscordAccount({ cfg: resolvedCfg, accountId });
|
||||
|
||||
// First try to resolve using directory lookup (handles usernames)
|
||||
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 resolveDiscordTarget(
|
||||
raw,
|
||||
{
|
||||
cfg: resolvedCfg,
|
||||
accountId: accountInfo.accountId,
|
||||
},
|
||||
parseOptions,
|
||||
);
|
||||
|
||||
if (resolved) {
|
||||
return { kind: resolved.kind, id: resolved.id };
|
||||
}
|
||||
|
||||
// Fallback to standard parsing (for channels, etc.)
|
||||
const parsed = parseDiscordTarget(raw, parseOptions);
|
||||
|
||||
if (!parsed) {
|
||||
throw new Error("Recipient is required for Discord sends");
|
||||
}
|
||||
|
||||
return { kind: parsed.kind, id: parsed.id };
|
||||
}
|
||||
|
||||
function normalizeStickerIds(raw: string[]) {
|
||||
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
|
|
@ -512,7 +451,6 @@ export {
|
|||
normalizeEmojiName,
|
||||
normalizeReactionEmoji,
|
||||
normalizeStickerIds,
|
||||
parseRecipient,
|
||||
resolveChannelId,
|
||||
resolveDiscordRest,
|
||||
sendDiscordMedia,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import type { DirectoryConfigParams } from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { buildMessagingTarget, type MessagingTarget } from "openclaw/plugin-sdk/messaging-targets";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { rememberDiscordDirectoryUser } from "./directory-cache.js";
|
||||
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
|
||||
import { parseDiscordSendTarget } from "./send-target-parsing.js";
|
||||
import { type DiscordTargetParseOptions } from "./target-parsing.js";
|
||||
|
||||
/**
|
||||
* Resolve a Discord username to user ID using the directory lookup.
|
||||
* This enables sending DMs by username instead of requiring explicit user IDs.
|
||||
*/
|
||||
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 ?? parseDiscordSendTarget(trimmed, parseOptions);
|
||||
}
|
||||
|
||||
try {
|
||||
const directoryEntries = await listDiscordDirectoryPeersLive({
|
||||
...options,
|
||||
query: trimmed,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const match = directoryEntries[0];
|
||||
if (match && match.kind === "user") {
|
||||
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 {
|
||||
// Preserve legacy fallback behavior for channel names and direct ids.
|
||||
}
|
||||
|
||||
return parseDiscordSendTarget(trimmed, parseOptions);
|
||||
}
|
||||
|
||||
export async function parseAndResolveDiscordTarget(
|
||||
raw: string,
|
||||
options: DirectoryConfigParams,
|
||||
parseOptions: DiscordTargetParseOptions = {},
|
||||
): Promise<MessagingTarget> {
|
||||
const resolved =
|
||||
(await resolveDiscordTarget(raw, options, parseOptions)) ??
|
||||
parseDiscordSendTarget(raw, parseOptions);
|
||||
if (!resolved) {
|
||||
throw new Error("Recipient is required for Discord sends");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function safeParseDiscordTarget(
|
||||
input: string,
|
||||
options: DiscordTargetParseOptions,
|
||||
): MessagingTarget | undefined {
|
||||
try {
|
||||
return parseDiscordSendTarget(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;
|
||||
}
|
||||
|
||||
function isLikelyUsername(input: string): boolean {
|
||||
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -7,10 +7,9 @@ import {
|
|||
adaptScopedAccountAccessor,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/channel-targets";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createChannelDirectoryAdapter,
|
||||
createRuntimeDirectoryLiveAdapter,
|
||||
|
|
@ -31,6 +30,7 @@ import {
|
|||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/target-resolver-runtime";
|
||||
import {
|
||||
listEnabledSlackAccounts,
|
||||
resolveDefaultSlackAccountId,
|
||||
|
|
@ -71,7 +71,7 @@ import {
|
|||
slackConfigAdapter,
|
||||
SLACK_CHANNEL,
|
||||
} from "./shared.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
import { parseSlackTarget } from "./target-parsing.js";
|
||||
import { buildSlackThreadingToolContext } from "./threading-tool-context.js";
|
||||
|
||||
const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount>({
|
||||
|
|
|
|||
|
|
@ -1,99 +1,10 @@
|
|||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import type { PluginPackageChannel } from "../plugins/manifest.js";
|
||||
import { buildChatChannelMetaById, type ChatChannelMeta } from "./chat-meta-shared.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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
const CHAT_CHANNEL_META = buildChatChannelMetaById();
|
||||
|
||||
export type { ChatChannelMeta };
|
||||
|
||||
export function listChatChannels(): ChatChannelMeta[] {
|
||||
return CHAT_CHANNEL_ORDER.map((id) => CHAT_CHANNEL_META[id]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createJiti } from "jiti";
|
||||
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
|
|
@ -23,11 +24,49 @@ type GeneratedBundledChannelEntry = {
|
|||
};
|
||||
};
|
||||
|
||||
type BundledChannelDiscoveryCandidate = {
|
||||
rootDir: string;
|
||||
packageManifest?: {
|
||||
extensions?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("channels");
|
||||
|
||||
function resolveChannelPluginModuleEntry(
|
||||
moduleExport: unknown,
|
||||
): GeneratedBundledChannelEntry["entry"] | null {
|
||||
const resolveNamedFallback = (value: unknown): GeneratedBundledChannelEntry["entry"] | null => {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(value as Record<string, unknown>).filter(
|
||||
([key]) => key !== "default",
|
||||
);
|
||||
const pluginCandidates = entries.filter(
|
||||
([key, candidate]) =>
|
||||
key.endsWith("Plugin") &&
|
||||
!!candidate &&
|
||||
typeof candidate === "object" &&
|
||||
"id" in (candidate as Record<string, unknown>),
|
||||
);
|
||||
if (pluginCandidates.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const runtimeCandidates = entries.filter(
|
||||
([key, candidate]) =>
|
||||
key.startsWith("set") && key.endsWith("Runtime") && typeof candidate === "function",
|
||||
);
|
||||
return {
|
||||
channelPlugin: pluginCandidates[0][1] as ChannelPlugin,
|
||||
...(runtimeCandidates.length === 1
|
||||
? {
|
||||
setChannelRuntime: runtimeCandidates[0][1] as (runtime: PluginRuntime) => void,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
const resolved =
|
||||
moduleExport &&
|
||||
typeof moduleExport === "object" &&
|
||||
|
|
@ -42,7 +81,7 @@ function resolveChannelPluginModuleEntry(
|
|||
setChannelRuntime?: unknown;
|
||||
};
|
||||
if (!record.channelPlugin || typeof record.channelPlugin !== "object") {
|
||||
return null;
|
||||
return resolveNamedFallback(resolved) ?? resolveNamedFallback(moduleExport);
|
||||
}
|
||||
return {
|
||||
channelPlugin: record.channelPlugin as ChannelPlugin,
|
||||
|
|
@ -79,7 +118,8 @@ function createModuleLoader() {
|
|||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
|
||||
return (modulePath: string) => {
|
||||
const tryNative = shouldPreferNativeJiti(modulePath);
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
|
|
@ -101,9 +141,10 @@ function createModuleLoader() {
|
|||
const loadModule = createModuleLoader();
|
||||
|
||||
function loadBundledModule(modulePath: string, rootDir: string): unknown {
|
||||
const boundaryRoot = resolveCompiledBundledModulePath(rootDir);
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: modulePath,
|
||||
rootPath: rootDir,
|
||||
rootPath: boundaryRoot,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: false,
|
||||
skipLexicalRootCheck: true,
|
||||
|
|
@ -116,6 +157,29 @@ function loadBundledModule(modulePath: string, rootDir: string): unknown {
|
|||
return loadModule(safePath)(safePath);
|
||||
}
|
||||
|
||||
function resolveCompiledBundledModulePath(modulePath: string): string {
|
||||
const compiledDistModulePath = modulePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
return compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)
|
||||
? compiledDistModulePath
|
||||
: modulePath;
|
||||
}
|
||||
|
||||
function resolvePreferredBundledChannelSource(
|
||||
candidate: BundledChannelDiscoveryCandidate,
|
||||
manifest: ReturnType<typeof loadPluginManifestRegistry>["plugins"][number],
|
||||
): string {
|
||||
const declaredEntry = candidate.packageManifest?.extensions?.find(
|
||||
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
||||
);
|
||||
if (declaredEntry) {
|
||||
return resolveCompiledBundledModulePath(path.resolve(candidate.rootDir, declaredEntry));
|
||||
}
|
||||
return resolveCompiledBundledModulePath(manifest.source);
|
||||
}
|
||||
|
||||
function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelEntry[] {
|
||||
const discovery = discoverOpenClawPlugins({ cache: false });
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
|
|
@ -141,17 +205,23 @@ function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelE
|
|||
seenIds.add(manifest.id);
|
||||
|
||||
try {
|
||||
const sourcePath = resolvePreferredBundledChannelSource(candidate, manifest);
|
||||
const entry = resolveChannelPluginModuleEntry(
|
||||
loadBundledModule(candidate.source, candidate.rootDir),
|
||||
loadBundledModule(sourcePath, candidate.rootDir),
|
||||
);
|
||||
if (!entry) {
|
||||
log.warn(
|
||||
`[channels] bundled channel entry ${manifest.id} missing channelPlugin export; skipping`,
|
||||
`[channels] bundled channel entry ${manifest.id} missing channelPlugin export from ${sourcePath}; skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const setupEntry = manifest.setupSource
|
||||
? resolveChannelSetupModuleEntry(loadBundledModule(manifest.setupSource, candidate.rootDir))
|
||||
? resolveChannelSetupModuleEntry(
|
||||
loadBundledModule(
|
||||
resolveCompiledBundledModulePath(manifest.setupSource),
|
||||
candidate.rootDir,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
entries.push({
|
||||
id: manifest.id,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.core.js";
|
||||
import type { ChannelStatusIssue } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
|
|
@ -7,7 +6,6 @@ import {
|
|||
type ParsedChatTarget,
|
||||
} from "./channel-targets.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { asString, collectIssuesForEnabledAccounts, isRecord } from "./status-helpers.js";
|
||||
|
||||
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
|
||||
// Keep this list additive and scoped to the conversation-binding seam only.
|
||||
|
|
@ -27,6 +25,7 @@ type BlueBubblesFacadeModule = {
|
|||
accountId?: string;
|
||||
cfg: OpenClawConfig;
|
||||
}) => BlueBubblesConversationBindingManager;
|
||||
collectBlueBubblesStatusIssues: (accounts: unknown[]) => ChannelStatusIssue[];
|
||||
};
|
||||
|
||||
function loadBlueBubblesFacadeModule(): BlueBubblesFacadeModule {
|
||||
|
|
@ -266,99 +265,8 @@ export function resolveBlueBubblesConversationIdFromTarget(target: string): stri
|
|||
return normalizeBlueBubblesAcpConversationId(target)?.conversationId;
|
||||
}
|
||||
|
||||
type BlueBubblesAccountStatus = {
|
||||
accountId?: unknown;
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
running?: unknown;
|
||||
baseUrl?: unknown;
|
||||
lastError?: unknown;
|
||||
probe?: unknown;
|
||||
};
|
||||
|
||||
type BlueBubblesProbeResult = {
|
||||
ok?: boolean;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
baseUrl: value.baseUrl,
|
||||
lastError: value.lastError,
|
||||
probe: value.probe,
|
||||
};
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
error: asString(value.error) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectBlueBubblesStatusIssues(
|
||||
accounts: ChannelAccountSnapshot[],
|
||||
): ChannelStatusIssue[] {
|
||||
return collectIssuesForEnabledAccounts({
|
||||
accounts,
|
||||
readAccount: readBlueBubblesAccountStatus,
|
||||
collectIssues: ({ account, accountId, issues }) => {
|
||||
const configured = account.configured === true;
|
||||
const running = account.running === true;
|
||||
const lastError = asString(account.lastError);
|
||||
const probe = readBlueBubblesProbeResult(account.probe);
|
||||
|
||||
if (!configured) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Not configured (missing serverUrl or password).",
|
||||
fix: "Run: openclaw channels add bluebubbles --http-url <server-url> --password <password>",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (probe && probe.ok === false) {
|
||||
const errorDetail = probe.error
|
||||
? `: ${probe.error}`
|
||||
: probe.status
|
||||
? ` (HTTP ${probe.status})`
|
||||
: "";
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `BlueBubbles server unreachable${errorDetail}`,
|
||||
fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.",
|
||||
});
|
||||
}
|
||||
|
||||
if (running && lastError) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `Channel error: ${lastError}`,
|
||||
fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
export function collectBlueBubblesStatusIssues(accounts: unknown[]): ChannelStatusIssue[] {
|
||||
return loadBlueBubblesFacadeModule().collectBlueBubblesStatusIssues(accounts);
|
||||
}
|
||||
|
||||
export { resolveAckReaction } from "../agents/identity.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
createScopedAccountReplyToModeResolver,
|
||||
createTopLevelChannelReplyToModeResolver,
|
||||
} from "../channels/plugins/threading-helpers.js";
|
||||
import type {
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPairingAdapter,
|
||||
ChannelSecurityAdapter,
|
||||
} from "../channels/plugins/types.adapters.js";
|
||||
import type {
|
||||
ChannelMessagingAdapter,
|
||||
ChannelOutboundSessionRoute,
|
||||
ChannelPollResult,
|
||||
ChannelThreadingAdapter,
|
||||
} from "../channels/plugins/types.core.js";
|
||||
import type { ChannelConfigUiHint, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ReplyToMode } from "../config/types.base.js";
|
||||
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
|
||||
|
||||
export type { ChannelConfigUiHint, ChannelPlugin };
|
||||
export type { OpenClawConfig };
|
||||
export type { PluginRuntime };
|
||||
|
||||
export type ChannelOutboundSessionRouteParams = Parameters<
|
||||
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
|
||||
>[0];
|
||||
|
||||
type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
plugin: TPlugin;
|
||||
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
|
||||
setRuntime?: (runtime: PluginRuntime) => void;
|
||||
registerCliMetadata?: (api: OpenClawPluginApi) => void;
|
||||
registerFull?: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
type DefinedChannelPluginEntry<TPlugin> = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
configSchema: OpenClawPluginConfigSchema;
|
||||
register: (api: OpenClawPluginApi) => void;
|
||||
channelPlugin: TPlugin;
|
||||
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
||||
};
|
||||
|
||||
type ChatChannelPluginBase<TResolvedAccount, Probe, Audit> = Omit<
|
||||
ChannelPlugin<TResolvedAccount, Probe, Audit>,
|
||||
"security" | "pairing" | "threading" | "outbound"
|
||||
> &
|
||||
Partial<
|
||||
Pick<
|
||||
ChannelPlugin<TResolvedAccount, Probe, Audit>,
|
||||
"security" | "pairing" | "threading" | "outbound"
|
||||
>
|
||||
>;
|
||||
|
||||
type ChatChannelSecurityOptions<TResolvedAccount extends { accountId?: string | null }> = {
|
||||
dm: {
|
||||
channelKey: string;
|
||||
resolvePolicy: (account: TResolvedAccount) => string | null | undefined;
|
||||
resolveAllowFrom: (account: TResolvedAccount) => Array<string | number> | null | undefined;
|
||||
resolveFallbackAccountId?: (account: TResolvedAccount) => string | null | undefined;
|
||||
defaultPolicy?: string;
|
||||
allowFromPathSuffix?: string;
|
||||
policyPathSuffix?: string;
|
||||
approveChannelId?: string;
|
||||
approveHint?: string;
|
||||
normalizeEntry?: (raw: string) => string;
|
||||
};
|
||||
collectWarnings?: ChannelSecurityAdapter<TResolvedAccount>["collectWarnings"];
|
||||
collectAuditFindings?: ChannelSecurityAdapter<TResolvedAccount>["collectAuditFindings"];
|
||||
};
|
||||
|
||||
type ChatChannelPairingOptions = {
|
||||
text: {
|
||||
idLabel: string;
|
||||
message: string;
|
||||
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
||||
notify: (
|
||||
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
||||
message: string;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
};
|
||||
};
|
||||
|
||||
type ChatChannelThreadingReplyModeOptions<TResolvedAccount> =
|
||||
| { topLevelReplyToMode: string }
|
||||
| {
|
||||
scopedAccountReplyToMode: {
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => TResolvedAccount;
|
||||
resolveReplyToMode: (
|
||||
account: TResolvedAccount,
|
||||
chatType?: string | null,
|
||||
) => ReplyToMode | null | undefined;
|
||||
fallback?: ReplyToMode;
|
||||
};
|
||||
}
|
||||
| {
|
||||
resolveReplyToMode: NonNullable<ChannelThreadingAdapter["resolveReplyToMode"]>;
|
||||
};
|
||||
|
||||
type ChatChannelThreadingOptions<TResolvedAccount> =
|
||||
ChatChannelThreadingReplyModeOptions<TResolvedAccount> &
|
||||
Omit<ChannelThreadingAdapter, "resolveReplyToMode">;
|
||||
|
||||
type ChatChannelAttachedOutboundOptions = {
|
||||
base: Omit<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
||||
attachedResults: {
|
||||
channel: string;
|
||||
sendText?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0],
|
||||
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendMedia?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0],
|
||||
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendPoll?: (
|
||||
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0],
|
||||
) => MaybePromise<Omit<ChannelPollResult, "channel">>;
|
||||
};
|
||||
};
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
function createInlineTextPairingAdapter(params: {
|
||||
idLabel: string;
|
||||
message: string;
|
||||
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
||||
notify: (
|
||||
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
||||
message: string;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
}): ChannelPairingAdapter {
|
||||
return {
|
||||
idLabel: params.idLabel,
|
||||
normalizeAllowEntry: params.normalizeAllowEntry,
|
||||
notifyApproval: async (ctx) => {
|
||||
await params.notify({
|
||||
...ctx,
|
||||
message: params.message,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInlineAttachedChannelResultAdapter(
|
||||
params: ChatChannelAttachedOutboundOptions["attachedResults"],
|
||||
) {
|
||||
return {
|
||||
sendText: params.sendText
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendText!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
sendMedia: params.sendMedia
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendMedia!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
sendPoll: params.sendPoll
|
||||
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0]) => ({
|
||||
channel: params.channel,
|
||||
...(await params.sendPoll!(ctx)),
|
||||
})
|
||||
: undefined,
|
||||
} satisfies Pick<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
||||
}
|
||||
|
||||
function resolveChatChannelSecurity<TResolvedAccount extends { accountId?: string | null }>(
|
||||
security:
|
||||
| ChannelSecurityAdapter<TResolvedAccount>
|
||||
| ChatChannelSecurityOptions<TResolvedAccount>
|
||||
| undefined,
|
||||
): ChannelSecurityAdapter<TResolvedAccount> | undefined {
|
||||
if (!security) {
|
||||
return undefined;
|
||||
}
|
||||
if (!("dm" in security)) {
|
||||
return security;
|
||||
}
|
||||
return {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: security.dm.channelKey,
|
||||
accountId,
|
||||
fallbackAccountId: security.dm.resolveFallbackAccountId?.(account) ?? account.accountId,
|
||||
policy: security.dm.resolvePolicy(account),
|
||||
allowFrom: security.dm.resolveAllowFrom(account) ?? [],
|
||||
defaultPolicy: security.dm.defaultPolicy,
|
||||
allowFromPathSuffix: security.dm.allowFromPathSuffix,
|
||||
policyPathSuffix: security.dm.policyPathSuffix,
|
||||
approveChannelId: security.dm.approveChannelId,
|
||||
approveHint: security.dm.approveHint,
|
||||
normalizeEntry: security.dm.normalizeEntry,
|
||||
}),
|
||||
...(security.collectWarnings ? { collectWarnings: security.collectWarnings } : {}),
|
||||
...(security.collectAuditFindings
|
||||
? { collectAuditFindings: security.collectAuditFindings }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChatChannelPairing(
|
||||
pairing: ChannelPairingAdapter | ChatChannelPairingOptions | undefined,
|
||||
): ChannelPairingAdapter | undefined {
|
||||
if (!pairing) {
|
||||
return undefined;
|
||||
}
|
||||
if (!("text" in pairing)) {
|
||||
return pairing;
|
||||
}
|
||||
return createInlineTextPairingAdapter(pairing.text);
|
||||
}
|
||||
|
||||
function resolveChatChannelThreading<TResolvedAccount>(
|
||||
threading: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount> | undefined,
|
||||
): ChannelThreadingAdapter | undefined {
|
||||
if (!threading) {
|
||||
return undefined;
|
||||
}
|
||||
if (!("topLevelReplyToMode" in threading) && !("scopedAccountReplyToMode" in threading)) {
|
||||
return threading;
|
||||
}
|
||||
|
||||
let resolveReplyToMode: ChannelThreadingAdapter["resolveReplyToMode"];
|
||||
if ("topLevelReplyToMode" in threading) {
|
||||
resolveReplyToMode = createTopLevelChannelReplyToModeResolver(threading.topLevelReplyToMode);
|
||||
} else {
|
||||
resolveReplyToMode = createScopedAccountReplyToModeResolver<TResolvedAccount>(
|
||||
threading.scopedAccountReplyToMode,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...threading,
|
||||
resolveReplyToMode,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChatChannelOutbound(
|
||||
outbound: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions | undefined,
|
||||
): ChannelOutboundAdapter | undefined {
|
||||
if (!outbound) {
|
||||
return undefined;
|
||||
}
|
||||
if (!("attachedResults" in outbound)) {
|
||||
return outbound;
|
||||
}
|
||||
return {
|
||||
...outbound.base,
|
||||
...createInlineAttachedChannelResultAdapter(outbound.attachedResults),
|
||||
};
|
||||
}
|
||||
|
||||
export function defineChannelPluginEntry<TPlugin>({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
plugin,
|
||||
configSchema = emptyPluginConfigSchema,
|
||||
setRuntime,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
|
||||
const resolvedConfigSchema = typeof configSchema === "function" ? configSchema() : configSchema;
|
||||
const entry = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
configSchema: resolvedConfigSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
if (api.registrationMode === "cli-metadata") {
|
||||
registerCliMetadata?.(api);
|
||||
return;
|
||||
}
|
||||
setRuntime?.(api.runtime);
|
||||
api.registerChannel({ plugin: plugin as ChannelPlugin });
|
||||
if (api.registrationMode !== "full") {
|
||||
return;
|
||||
}
|
||||
registerCliMetadata?.(api);
|
||||
registerFull?.(api);
|
||||
},
|
||||
};
|
||||
return {
|
||||
...entry,
|
||||
channelPlugin: plugin,
|
||||
...(setRuntime ? { setChannelRuntime: setRuntime } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin) {
|
||||
return { plugin };
|
||||
}
|
||||
|
||||
export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string {
|
||||
const trimmed = raw.trim();
|
||||
for (const provider of providers) {
|
||||
const prefix = `${provider.toLowerCase()}:`;
|
||||
if (trimmed.toLowerCase().startsWith(prefix)) {
|
||||
return trimmed.slice(prefix.length).trim();
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function stripTargetKindPrefix(raw: string): string {
|
||||
return raw.replace(/^(user|channel|group|conversation|room|dm):/i, "").trim();
|
||||
}
|
||||
|
||||
export function buildChannelOutboundSessionRoute(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
channel: string;
|
||||
accountId?: string | null;
|
||||
peer: { kind: "direct" | "group" | "channel"; id: string };
|
||||
chatType: "direct" | "group" | "channel";
|
||||
from: string;
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
}): ChannelOutboundSessionRoute {
|
||||
const baseSessionKey = buildOutboundBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
peer: params.peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer: params.peer,
|
||||
chatType: params.chatType,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatChannelPlugin<
|
||||
TResolvedAccount extends { accountId?: string | null },
|
||||
Probe = unknown,
|
||||
Audit = unknown,
|
||||
>(params: {
|
||||
base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>;
|
||||
security?:
|
||||
| ChannelSecurityAdapter<TResolvedAccount>
|
||||
| ChatChannelSecurityOptions<TResolvedAccount>;
|
||||
pairing?: ChannelPairingAdapter | ChatChannelPairingOptions;
|
||||
threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>;
|
||||
outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions;
|
||||
}): ChannelPlugin<TResolvedAccount, Probe, Audit> {
|
||||
return {
|
||||
...params.base,
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
...params.base.conversationBindings,
|
||||
},
|
||||
...(params.security ? { security: resolveChatChannelSecurity(params.security) } : {}),
|
||||
...(params.pairing ? { pairing: resolveChatChannelPairing(params.pairing) } : {}),
|
||||
...(params.threading ? { threading: resolveChatChannelThreading(params.threading) } : {}),
|
||||
...(params.outbound ? { outbound: resolveChatChannelOutbound(params.outbound) } : {}),
|
||||
} as ChannelPlugin<TResolvedAccount, Probe, Audit>;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../channels/ids.js";
|
||||
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
createScopedAccountReplyToModeResolver,
|
||||
|
|
@ -15,12 +15,15 @@ import type {
|
|||
ChannelPollResult,
|
||||
ChannelThreadingAdapter,
|
||||
} from "../channels/plugins/types.core.js";
|
||||
import type { ChannelMeta } from "../channels/plugins/types.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ReplyToMode } from "../config/types.base.js";
|
||||
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
import type { PluginPackageChannel } from "../plugins/manifest.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
|
||||
|
||||
|
|
@ -155,7 +158,6 @@ export {
|
|||
formatPairingApproveHint,
|
||||
parseOptionalDelimitedEntries,
|
||||
} from "../channels/plugins/helpers.js";
|
||||
export { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
export {
|
||||
channelTargetSchema,
|
||||
channelTargetsSchema,
|
||||
|
|
@ -205,6 +207,105 @@ export type ChannelOutboundSessionRouteParams = Parameters<
|
|||
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
|
||||
>[0];
|
||||
|
||||
var cachedSdkChatChannelMeta: ReturnType<typeof buildChatChannelMetaById> | undefined;
|
||||
var cachedSdkChatChannelIdSet: Set<string> | undefined;
|
||||
|
||||
function getSdkChatChannelIdSet(): Set<string> {
|
||||
cachedSdkChatChannelIdSet ??= new Set(CHAT_CHANNEL_ORDER);
|
||||
return cachedSdkChatChannelIdSet;
|
||||
}
|
||||
|
||||
function toSdkChatChannelMeta(params: {
|
||||
id: ChatChannelId;
|
||||
channel: PluginPackageChannel;
|
||||
}): ChannelMeta {
|
||||
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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildChatChannelMetaById(): Record<ChatChannelId, ChannelMeta> {
|
||||
const entries = new Map<ChatChannelId, ChannelMeta>();
|
||||
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 || !getSdkChatChannelIdSet().has(rawId)) {
|
||||
continue;
|
||||
}
|
||||
const id = rawId;
|
||||
entries.set(
|
||||
id,
|
||||
toSdkChatChannelMeta({
|
||||
id,
|
||||
channel,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Object.freeze(Object.fromEntries(entries)) as Record<ChatChannelId, ChannelMeta>;
|
||||
}
|
||||
|
||||
function resolveSdkChatChannelMeta(id: string) {
|
||||
cachedSdkChatChannelMeta ??= buildChatChannelMetaById();
|
||||
return cachedSdkChatChannelMeta[id];
|
||||
}
|
||||
|
||||
export function getChatChannelMeta(id: ChatChannelId): ChannelMeta {
|
||||
return resolveSdkChatChannelMeta(id);
|
||||
}
|
||||
|
||||
/** Remove one of the known provider prefixes from a free-form target string. */
|
||||
export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string {
|
||||
const trimmed = raw.trim();
|
||||
|
|
@ -604,7 +705,7 @@ export function createChannelPluginBase<TResolvedAccount>(
|
|||
return {
|
||||
id: params.id,
|
||||
meta: {
|
||||
...getChatChannelMeta(params.id as Parameters<typeof getChatChannelMeta>[0]),
|
||||
...resolveSdkChatChannelMeta(params.id),
|
||||
...params.meta,
|
||||
},
|
||||
...(params.setupWizard ? { setupWizard: params.setupWizard } : {}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue